journey tracks content linter rules (#57909)
This commit is contained in:
@@ -73,6 +73,9 @@
|
|||||||
| GHD055 | frontmatter-validation | Frontmatter properties must meet character limits and required property requirements | warning | frontmatter, character-limits, required-properties |
|
| GHD055 | frontmatter-validation | Frontmatter properties must meet character limits and required property requirements | warning | frontmatter, character-limits, required-properties |
|
||||||
| GHD056 | frontmatter-landing-recommended | Only landing pages can have recommended articles, there should be no duplicate recommended articles, and all recommended articles must exist | error | frontmatter, landing, recommended |
|
| GHD056 | frontmatter-landing-recommended | Only landing pages can have recommended articles, there should be no duplicate recommended articles, and all recommended articles must exist | error | frontmatter, landing, recommended |
|
||||||
| GHD057 | ctas-schema | CTA URLs must conform to the schema | error | ctas, schema, urls |
|
| GHD057 | ctas-schema | CTA URLs must conform to the schema | error | ctas, schema, urls |
|
||||||
|
| GHD058 | journey-tracks-liquid | Journey track properties must use valid Liquid syntax | error | frontmatter, journey-tracks, liquid |
|
||||||
|
| GHD059 | journey-tracks-guide-path-exists | Journey track guide paths must reference existing content files | error | frontmatter, journey-tracks |
|
||||||
|
| GHD060 | journey-tracks-unique-ids | Journey track IDs must be unique within a page | error | frontmatter, journey-tracks, unique-ids |
|
||||||
| [search-replace](https://github.com/OnkarRuikar/markdownlint-rule-search-replace) | deprecated liquid syntax: octicon-<icon-name> | The octicon liquid syntax used is deprecated. Use this format instead `octicon "<octicon-name>" aria-label="<Octicon aria label>"` | error | |
|
| [search-replace](https://github.com/OnkarRuikar/markdownlint-rule-search-replace) | deprecated liquid syntax: octicon-<icon-name> | The octicon liquid syntax used is deprecated. Use this format instead `octicon "<octicon-name>" aria-label="<Octicon aria label>"` | error | |
|
||||||
| [search-replace](https://github.com/OnkarRuikar/markdownlint-rule-search-replace) | deprecated liquid syntax: site.data | Catch occurrences of deprecated liquid data syntax. | error | |
|
| [search-replace](https://github.com/OnkarRuikar/markdownlint-rule-search-replace) | deprecated liquid syntax: site.data | Catch occurrences of deprecated liquid data syntax. | error | |
|
||||||
| [search-replace](https://github.com/OnkarRuikar/markdownlint-rule-search-replace) | developer-domain | Catch occurrences of developer.github.com domain. | error | |
|
| [search-replace](https://github.com/OnkarRuikar/markdownlint-rule-search-replace) | developer-domain | Catch occurrences of developer.github.com domain. | error | |
|
||||||
|
|||||||
@@ -58,6 +58,9 @@ import { headerContentRequirement } from '@/content-linter/lib/linting-rules/hea
|
|||||||
import { thirdPartyActionsReusable } from '@/content-linter/lib/linting-rules/third-party-actions-reusable'
|
import { thirdPartyActionsReusable } from '@/content-linter/lib/linting-rules/third-party-actions-reusable'
|
||||||
import { frontmatterLandingRecommended } from '@/content-linter/lib/linting-rules/frontmatter-landing-recommended'
|
import { frontmatterLandingRecommended } from '@/content-linter/lib/linting-rules/frontmatter-landing-recommended'
|
||||||
import { ctasSchema } from '@/content-linter/lib/linting-rules/ctas-schema'
|
import { ctasSchema } from '@/content-linter/lib/linting-rules/ctas-schema'
|
||||||
|
import { journeyTracksLiquid } from './journey-tracks-liquid'
|
||||||
|
import { journeyTracksGuidePathExists } from './journey-tracks-guide-path-exists'
|
||||||
|
import { journeyTracksUniqueIds } from './journey-tracks-unique-ids'
|
||||||
|
|
||||||
// Using any type because @github/markdownlint-github doesn't provide TypeScript declarations
|
// Using any type because @github/markdownlint-github doesn't provide TypeScript declarations
|
||||||
// The elements in the array have a 'names' property that contains rule identifiers
|
// The elements in the array have a 'names' property that contains rule identifiers
|
||||||
@@ -124,6 +127,9 @@ export const gitHubDocsMarkdownlint = {
|
|||||||
frontmatterValidation, // GHD055
|
frontmatterValidation, // GHD055
|
||||||
frontmatterLandingRecommended, // GHD056
|
frontmatterLandingRecommended, // GHD056
|
||||||
ctasSchema, // GHD057
|
ctasSchema, // GHD057
|
||||||
|
journeyTracksLiquid, // GHD058
|
||||||
|
journeyTracksGuidePathExists, // GHD059
|
||||||
|
journeyTracksUniqueIds, // GHD060
|
||||||
|
|
||||||
// Search-replace rules
|
// Search-replace rules
|
||||||
searchReplace, // Open-source plugin
|
searchReplace, // Open-source plugin
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
// @ts-ignore - markdownlint-rule-helpers doesn't have TypeScript declarations
|
||||||
|
import { addError } from 'markdownlint-rule-helpers'
|
||||||
|
|
||||||
|
import { getFrontmatter } from '../helpers/utils'
|
||||||
|
import type { RuleParams, RuleErrorCallback } from '@/content-linter/types'
|
||||||
|
|
||||||
|
// Yoink path validation approach from frontmatter-landing-recommended
|
||||||
|
function isValidGuidePath(guidePath: string, currentFilePath: string): boolean {
|
||||||
|
const ROOT = process.env.ROOT || '.'
|
||||||
|
|
||||||
|
// Strategy 1: Always try as an absolute path from content root first
|
||||||
|
const contentDir = path.join(ROOT, 'content')
|
||||||
|
const normalizedPath = guidePath.startsWith('/') ? guidePath.substring(1) : guidePath
|
||||||
|
const absolutePath = path.join(contentDir, `${normalizedPath}.md`)
|
||||||
|
|
||||||
|
if (fs.existsSync(absolutePath) && fs.statSync(absolutePath).isFile()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy 2: Fall back to relative path from current file's directory
|
||||||
|
const currentDir = path.dirname(currentFilePath)
|
||||||
|
const relativePath = path.join(currentDir, `${normalizedPath}.md`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
return fs.existsSync(relativePath) && fs.statSync(relativePath).isFile()
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const journeyTracksGuidePathExists = {
|
||||||
|
names: ['GHD059', 'journey-tracks-guide-path-exists'],
|
||||||
|
description: 'Journey track guide paths must reference existing content files',
|
||||||
|
tags: ['frontmatter', 'journey-tracks'],
|
||||||
|
function: (params: RuleParams, onError: RuleErrorCallback) => {
|
||||||
|
// Using any for frontmatter as it's a dynamic YAML object with varying properties
|
||||||
|
const fm: any = getFrontmatter(params.lines)
|
||||||
|
if (!fm || !fm.journeyTracks || !Array.isArray(fm.journeyTracks)) return
|
||||||
|
if (!fm.layout || fm.layout !== 'journey-landing') return
|
||||||
|
|
||||||
|
const journeyTracksLine = params.lines.find((line: string) => line.startsWith('journeyTracks:'))
|
||||||
|
|
||||||
|
if (!journeyTracksLine) return
|
||||||
|
|
||||||
|
const journeyTracksLineNumber = params.lines.indexOf(journeyTracksLine) + 1
|
||||||
|
|
||||||
|
fm.journeyTracks.forEach((track: any, trackIndex: number) => {
|
||||||
|
if (track.guides && Array.isArray(track.guides)) {
|
||||||
|
track.guides.forEach((guide: string, guideIndex: number) => {
|
||||||
|
if (typeof guide === 'string') {
|
||||||
|
if (!isValidGuidePath(guide, params.name)) {
|
||||||
|
addError(
|
||||||
|
onError,
|
||||||
|
journeyTracksLineNumber,
|
||||||
|
`Journey track guide path does not exist: ${guide} (track ${trackIndex + 1}, guide ${guideIndex + 1})`,
|
||||||
|
guide,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
// @ts-ignore - markdownlint-rule-helpers doesn't have TypeScript declarations
|
||||||
|
import { addError } from 'markdownlint-rule-helpers'
|
||||||
|
|
||||||
|
import { getFrontmatter } from '../helpers/utils'
|
||||||
|
import { liquid } from '@/content-render/index'
|
||||||
|
import type { RuleParams, RuleErrorCallback } from '@/content-linter/types'
|
||||||
|
|
||||||
|
export const journeyTracksLiquid = {
|
||||||
|
names: ['GHD058', 'journey-tracks-liquid'],
|
||||||
|
description: 'Journey track properties must use valid Liquid syntax',
|
||||||
|
tags: ['frontmatter', 'journey-tracks', 'liquid'],
|
||||||
|
function: (params: RuleParams, onError: RuleErrorCallback) => {
|
||||||
|
// Using any for frontmatter as it's a dynamic YAML object with varying properties
|
||||||
|
const fm: any = getFrontmatter(params.lines)
|
||||||
|
if (!fm || !fm.journeyTracks || !Array.isArray(fm.journeyTracks)) return
|
||||||
|
if (!fm.layout || fm.layout !== 'journey-landing') return
|
||||||
|
|
||||||
|
// Find the base journeyTracks line
|
||||||
|
const journeyTracksLine: string | undefined = params.lines.find((line: string) =>
|
||||||
|
line.trim().startsWith('journeyTracks:'),
|
||||||
|
)
|
||||||
|
const baseLineNumber: number = journeyTracksLine
|
||||||
|
? params.lines.indexOf(journeyTracksLine) + 1
|
||||||
|
: 1
|
||||||
|
|
||||||
|
fm.journeyTracks.forEach((track: any, trackIndex: number) => {
|
||||||
|
// Try to find the line number for this specific journey track so we can use that for the error
|
||||||
|
// line number. Getting the exact line number is probably more work than it's worth for this
|
||||||
|
// particular rule.
|
||||||
|
|
||||||
|
// Look for the track by finding the nth occurrence of track-like patterns after journeyTracks
|
||||||
|
let trackLineNumber: number = baseLineNumber
|
||||||
|
if (journeyTracksLine) {
|
||||||
|
let trackCount: number = 0
|
||||||
|
for (let i = params.lines.indexOf(journeyTracksLine) + 1; i < params.lines.length; i++) {
|
||||||
|
const line: string = params.lines[i].trim()
|
||||||
|
// Look for track indicators (array item with id, title, or description)
|
||||||
|
if (
|
||||||
|
line.startsWith('- id:') ||
|
||||||
|
line.startsWith('- title:') ||
|
||||||
|
(line === '-' &&
|
||||||
|
i + 1 < params.lines.length &&
|
||||||
|
(params.lines[i + 1].trim().startsWith('id:') ||
|
||||||
|
params.lines[i + 1].trim().startsWith('title:')))
|
||||||
|
) {
|
||||||
|
if (trackCount === trackIndex) {
|
||||||
|
trackLineNumber = i + 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
trackCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple validation - just check if liquid can parse each string property
|
||||||
|
const properties = [
|
||||||
|
{ name: 'title', value: track.title },
|
||||||
|
{ name: 'description', value: track.description },
|
||||||
|
]
|
||||||
|
|
||||||
|
properties.forEach((prop) => {
|
||||||
|
if (prop.value && typeof prop.value === 'string') {
|
||||||
|
try {
|
||||||
|
liquid.parse(prop.value)
|
||||||
|
} catch (error: any) {
|
||||||
|
addError(
|
||||||
|
onError,
|
||||||
|
trackLineNumber,
|
||||||
|
`Invalid Liquid syntax in journey track ${prop.name} (track ${trackIndex + 1}): ${error.message}`,
|
||||||
|
prop.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (track.guides && Array.isArray(track.guides)) {
|
||||||
|
track.guides.forEach((guide: string, guideIndex: number) => {
|
||||||
|
if (typeof guide === 'string') {
|
||||||
|
try {
|
||||||
|
liquid.parse(guide)
|
||||||
|
} catch (error: any) {
|
||||||
|
addError(
|
||||||
|
onError,
|
||||||
|
trackLineNumber,
|
||||||
|
`Invalid Liquid syntax in journey track guide (track ${trackIndex + 1}, guide ${guideIndex + 1}): ${error.message}`,
|
||||||
|
guide,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
// @ts-ignore - markdownlint-rule-helpers doesn't have TypeScript declarations
|
||||||
|
import { addError } from 'markdownlint-rule-helpers'
|
||||||
|
|
||||||
|
import { getFrontmatter } from '../helpers/utils'
|
||||||
|
import type { RuleParams, RuleErrorCallback } from '@/content-linter/types'
|
||||||
|
|
||||||
|
// GHD060
|
||||||
|
export const journeyTracksUniqueIds = {
|
||||||
|
names: ['GHD060', 'journey-tracks-unique-ids'],
|
||||||
|
description: 'Journey track IDs must be unique within a page',
|
||||||
|
tags: ['frontmatter', 'journey-tracks', 'unique-ids'],
|
||||||
|
function: function GHD060(params: RuleParams, onError: RuleErrorCallback) {
|
||||||
|
// Using any for frontmatter as it's a dynamic YAML object with varying properties
|
||||||
|
const fm: any = getFrontmatter(params.lines)
|
||||||
|
if (!fm || !fm.journeyTracks || !Array.isArray(fm.journeyTracks)) return
|
||||||
|
if (!fm.layout || fm.layout !== 'journey-landing') return
|
||||||
|
|
||||||
|
// Find the base journeyTracks line
|
||||||
|
const journeyTracksLine: string | undefined = params.lines.find((line: string) =>
|
||||||
|
line.trim().startsWith('journeyTracks:'),
|
||||||
|
)
|
||||||
|
const baseLineNumber: number = journeyTracksLine
|
||||||
|
? params.lines.indexOf(journeyTracksLine) + 1
|
||||||
|
: 1
|
||||||
|
|
||||||
|
// Helper function to find line number for a specific track by index
|
||||||
|
function getTrackLineNumber(trackIndex: number): number {
|
||||||
|
if (!journeyTracksLine) return baseLineNumber
|
||||||
|
|
||||||
|
let trackCount = 0
|
||||||
|
for (let i = params.lines.indexOf(journeyTracksLine) + 1; i < params.lines.length; i++) {
|
||||||
|
const line = params.lines[i].trim()
|
||||||
|
// Look for any "- id:" line (journey track indicator)
|
||||||
|
if (line.startsWith('- id:')) {
|
||||||
|
if (trackCount === trackIndex) {
|
||||||
|
return i + 1
|
||||||
|
}
|
||||||
|
trackCount++
|
||||||
|
|
||||||
|
// Stop once we've found all the tracks we know exist
|
||||||
|
if (fm && fm.journeyTracks && trackCount >= fm.journeyTracks.length) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return baseLineNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track seen journey track IDs and line number for error reporting
|
||||||
|
const seenIds = new Map<string, number>()
|
||||||
|
|
||||||
|
fm.journeyTracks.forEach((track: any, index: number) => {
|
||||||
|
if (!track || typeof track !== 'object') return
|
||||||
|
|
||||||
|
const trackId = track.id
|
||||||
|
if (!trackId || typeof trackId !== 'string') return
|
||||||
|
|
||||||
|
const currentLineNumber = getTrackLineNumber(index)
|
||||||
|
|
||||||
|
if (seenIds.has(trackId)) {
|
||||||
|
const firstLineNumber = seenIds.get(trackId)
|
||||||
|
addError(
|
||||||
|
onError,
|
||||||
|
currentLineNumber,
|
||||||
|
`Journey track ID "${trackId}" is duplicated (first seen at line ${firstLineNumber}, duplicate at line ${currentLineNumber})`,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
seenIds.set(trackId, currentLineNumber)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -330,6 +330,24 @@ export const githubDocsFrontmatterConfig = {
|
|||||||
'partial-markdown-files': true,
|
'partial-markdown-files': true,
|
||||||
'yml-files': true,
|
'yml-files': true,
|
||||||
},
|
},
|
||||||
|
'journey-tracks-liquid': {
|
||||||
|
// GHD058
|
||||||
|
severity: 'error',
|
||||||
|
'partial-markdown-files': false,
|
||||||
|
'yml-files': false,
|
||||||
|
},
|
||||||
|
'journey-tracks-guide-path-exists': {
|
||||||
|
// GHD059
|
||||||
|
severity: 'error',
|
||||||
|
'partial-markdown-files': false,
|
||||||
|
'yml-files': false,
|
||||||
|
},
|
||||||
|
'journey-tracks-unique-ids': {
|
||||||
|
// GHD060
|
||||||
|
severity: 'error',
|
||||||
|
'partial-markdown-files': false,
|
||||||
|
'yml-files': false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configures rules from the `github/markdownlint-github` repo
|
// Configures rules from the `github/markdownlint-github` repo
|
||||||
|
|||||||
27
src/content-linter/tests/fixtures/journey-tracks/duplicate-ids.md
vendored
Normal file
27
src/content-linter/tests/fixtures/journey-tracks/duplicate-ids.md
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
title: Journey with Duplicate IDs
|
||||||
|
layout: journey-landing
|
||||||
|
versions:
|
||||||
|
fpt: '*'
|
||||||
|
ghec: '*'
|
||||||
|
ghes: '*'
|
||||||
|
topics:
|
||||||
|
- Testing
|
||||||
|
journeyTracks:
|
||||||
|
- id: duplicate-id
|
||||||
|
title: "First Track"
|
||||||
|
guides:
|
||||||
|
- /article-one
|
||||||
|
- id: unique-id
|
||||||
|
title: "Unique Track"
|
||||||
|
guides:
|
||||||
|
- /article-two
|
||||||
|
- id: duplicate-id
|
||||||
|
title: "Second Track with Same ID"
|
||||||
|
guides:
|
||||||
|
- /subdir/article-three
|
||||||
|
---
|
||||||
|
|
||||||
|
# Journey with Duplicate IDs
|
||||||
|
|
||||||
|
This journey landing page has duplicate track IDs.
|
||||||
21
src/content-linter/tests/fixtures/journey-tracks/invalid-paths.md
vendored
Normal file
21
src/content-linter/tests/fixtures/journey-tracks/invalid-paths.md
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
title: Journey with Invalid Paths
|
||||||
|
layout: journey-landing
|
||||||
|
versions:
|
||||||
|
fpt: '*'
|
||||||
|
ghec: '*'
|
||||||
|
ghes: '*'
|
||||||
|
topics:
|
||||||
|
- Testing
|
||||||
|
journeyTracks:
|
||||||
|
- id: track-1
|
||||||
|
title: "Track with Invalid Guides"
|
||||||
|
guides:
|
||||||
|
- /article-one
|
||||||
|
- /nonexistent/guide
|
||||||
|
- /another/invalid/path
|
||||||
|
---
|
||||||
|
|
||||||
|
# Journey with Invalid Paths
|
||||||
|
|
||||||
|
This journey landing page has some invalid guide paths.
|
||||||
14
src/content-linter/tests/fixtures/journey-tracks/no-journey-tracks.md
vendored
Normal file
14
src/content-linter/tests/fixtures/journey-tracks/no-journey-tracks.md
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
title: Journey without Journey Tracks
|
||||||
|
layout: journey-landing
|
||||||
|
versions:
|
||||||
|
fpt: '*'
|
||||||
|
ghec: '*'
|
||||||
|
ghes: '*'
|
||||||
|
topics:
|
||||||
|
- Testing
|
||||||
|
---
|
||||||
|
|
||||||
|
# Journey without Journey Tracks
|
||||||
|
|
||||||
|
This journey landing page has no journeyTracks property.
|
||||||
19
src/content-linter/tests/fixtures/journey-tracks/non-journey-layout.md
vendored
Normal file
19
src/content-linter/tests/fixtures/journey-tracks/non-journey-layout.md
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
title: Non-Journey Page
|
||||||
|
layout: default
|
||||||
|
versions:
|
||||||
|
fpt: '*'
|
||||||
|
ghec: '*'
|
||||||
|
ghes: '*'
|
||||||
|
topics:
|
||||||
|
- Testing
|
||||||
|
journeyTracks:
|
||||||
|
- id: track-1
|
||||||
|
title: "Should be ignored"
|
||||||
|
guides:
|
||||||
|
- /nonexistent/path
|
||||||
|
---
|
||||||
|
|
||||||
|
# Non-Journey Page
|
||||||
|
|
||||||
|
This is not a journey-landing layout, so journeyTracks should be ignored.
|
||||||
24
src/content-linter/tests/fixtures/journey-tracks/valid-journey.md
vendored
Normal file
24
src/content-linter/tests/fixtures/journey-tracks/valid-journey.md
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
title: Valid Journey Landing
|
||||||
|
layout: journey-landing
|
||||||
|
versions:
|
||||||
|
fpt: '*'
|
||||||
|
ghec: '*'
|
||||||
|
ghes: '*'
|
||||||
|
topics:
|
||||||
|
- Testing
|
||||||
|
journeyTracks:
|
||||||
|
- id: track-1
|
||||||
|
title: "Getting Started Track"
|
||||||
|
guides:
|
||||||
|
- /article-one
|
||||||
|
- /article-two
|
||||||
|
- id: track-2
|
||||||
|
title: "Advanced Track"
|
||||||
|
guides:
|
||||||
|
- /subdir/article-three
|
||||||
|
---
|
||||||
|
|
||||||
|
# Valid Journey Landing
|
||||||
|
|
||||||
|
This is a valid journey landing page with valid guide paths.
|
||||||
130
src/content-linter/tests/unit/journey-tracks.ts
Normal file
130
src/content-linter/tests/unit/journey-tracks.ts
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import { describe, expect, test, beforeAll, afterAll } from 'vitest'
|
||||||
|
|
||||||
|
import { runRule } from '../../lib/init-test'
|
||||||
|
import { journeyTracksLiquid } from '../../lib/linting-rules/journey-tracks-liquid'
|
||||||
|
import { journeyTracksGuidePathExists } from '../../lib/linting-rules/journey-tracks-guide-path-exists'
|
||||||
|
import { journeyTracksUniqueIds } from '../../lib/linting-rules/journey-tracks-unique-ids'
|
||||||
|
|
||||||
|
const VALID_JOURNEY = 'src/content-linter/tests/fixtures/journey-tracks/valid-journey.md'
|
||||||
|
const INVALID_PATHS = 'src/content-linter/tests/fixtures/journey-tracks/invalid-paths.md'
|
||||||
|
const NON_JOURNEY_LAYOUT = 'src/content-linter/tests/fixtures/journey-tracks/non-journey-layout.md'
|
||||||
|
const DUPLICATE_IDS = 'src/content-linter/tests/fixtures/journey-tracks/duplicate-ids.md'
|
||||||
|
const NO_JOURNEY_TRACKS = 'src/content-linter/tests/fixtures/journey-tracks/no-journey-tracks.md'
|
||||||
|
|
||||||
|
const fmOptions = { markdownlintOptions: { frontMatter: null } }
|
||||||
|
|
||||||
|
describe('journey-tracks-liquid', () => {
|
||||||
|
test('valid liquid syntax passes', async () => {
|
||||||
|
const result = await runRule(journeyTracksLiquid, {
|
||||||
|
files: [VALID_JOURNEY],
|
||||||
|
...fmOptions,
|
||||||
|
})
|
||||||
|
expect(result[VALID_JOURNEY]).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('invalid liquid syntax fails', async () => {
|
||||||
|
// Using inline content instead of a fixture file to avoid CI conflicts.
|
||||||
|
// Malformed Liquid syntax in fixture files causes other rules (like liquid-versioning)
|
||||||
|
// to crash when they try to parse the same file during content linting.
|
||||||
|
const invalidLiquidContent = `---
|
||||||
|
title: Journey with Liquid Syntax
|
||||||
|
layout: journey-landing
|
||||||
|
versions:
|
||||||
|
fpt: '*'
|
||||||
|
ghec: '*'
|
||||||
|
ghes: '*'
|
||||||
|
topics:
|
||||||
|
- Testing
|
||||||
|
journeyTracks:
|
||||||
|
- id: track-1
|
||||||
|
title: "Track with {% invalid liquid"
|
||||||
|
description: "Description with {{ unclosed liquid"
|
||||||
|
guides:
|
||||||
|
- /article-one
|
||||||
|
---
|
||||||
|
|
||||||
|
# Journey with Liquid Issues
|
||||||
|
|
||||||
|
This journey landing page has invalid liquid syntax in journeyTracks.
|
||||||
|
`
|
||||||
|
const result = await runRule(journeyTracksLiquid, {
|
||||||
|
strings: { 'test-invalid-liquid.md': invalidLiquidContent },
|
||||||
|
...fmOptions,
|
||||||
|
})
|
||||||
|
expect(result['test-invalid-liquid.md']).toHaveLength(2) // title and description both have invalid liquid
|
||||||
|
expect(result['test-invalid-liquid.md'][0].ruleDescription).toMatch(/liquid syntax/i)
|
||||||
|
expect(result['test-invalid-liquid.md'][1].ruleDescription).toMatch(/liquid syntax/i)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('journey-tracks-guide-path-exists', () => {
|
||||||
|
const envVarValueBefore = process.env.ROOT
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
process.env.ROOT = 'src/fixtures/fixtures'
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
process.env.ROOT = envVarValueBefore
|
||||||
|
})
|
||||||
|
|
||||||
|
test('ignores non-journey-landing layouts', async () => {
|
||||||
|
const result = await runRule(journeyTracksGuidePathExists, {
|
||||||
|
files: [NON_JOURNEY_LAYOUT],
|
||||||
|
...fmOptions,
|
||||||
|
})
|
||||||
|
expect(result[NON_JOURNEY_LAYOUT]).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('valid guide paths pass', async () => {
|
||||||
|
const result = await runRule(journeyTracksGuidePathExists, {
|
||||||
|
files: [VALID_JOURNEY],
|
||||||
|
...fmOptions,
|
||||||
|
})
|
||||||
|
expect(result[VALID_JOURNEY]).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('invalid guide paths fail', async () => {
|
||||||
|
const result = await runRule(journeyTracksGuidePathExists, {
|
||||||
|
files: [INVALID_PATHS],
|
||||||
|
...fmOptions,
|
||||||
|
})
|
||||||
|
expect(result[INVALID_PATHS].length).toBeGreaterThan(0)
|
||||||
|
expect(result[INVALID_PATHS][0].errorDetail).toContain('does not exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('pages without journey tracks pass', async () => {
|
||||||
|
const result = await runRule(journeyTracksGuidePathExists, {
|
||||||
|
files: [NO_JOURNEY_TRACKS],
|
||||||
|
...fmOptions,
|
||||||
|
})
|
||||||
|
expect(result[NO_JOURNEY_TRACKS]).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('journey-tracks-unique-ids', () => {
|
||||||
|
test('unique IDs pass', async () => {
|
||||||
|
const result = await runRule(journeyTracksUniqueIds, {
|
||||||
|
files: [VALID_JOURNEY],
|
||||||
|
...fmOptions,
|
||||||
|
})
|
||||||
|
expect(result[VALID_JOURNEY]).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('duplicate IDs fail', async () => {
|
||||||
|
const result = await runRule(journeyTracksUniqueIds, {
|
||||||
|
files: [DUPLICATE_IDS],
|
||||||
|
...fmOptions,
|
||||||
|
})
|
||||||
|
expect(result[DUPLICATE_IDS]).toHaveLength(1)
|
||||||
|
expect(result[DUPLICATE_IDS][0].errorDetail).toContain('duplicate-id')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('ignores non-journey-landing layouts', async () => {
|
||||||
|
const result = await runRule(journeyTracksUniqueIds, {
|
||||||
|
files: [NON_JOURNEY_LAYOUT],
|
||||||
|
...fmOptions,
|
||||||
|
})
|
||||||
|
expect(result[NON_JOURNEY_LAYOUT]).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user