REST subcategory/category rendering test and refactor test-open-api-schema (#27138)
* first stage of test * update test * add rest test for categories and subcategories rendering * update timeout back to original * remove export * remove testing * refactor test-open-api-schema * remove function * remove check * remove slash * remove comment * rearrange * update getting the categories and maptopic levels * update tests * update copy * add error message task
This commit is contained in:
@@ -149,7 +149,10 @@ export const RestCollapsibleSection = (props: SectionProps) => {
|
||||
)}
|
||||
>
|
||||
<div className="d-flex flex-justify-between">
|
||||
<div className="pl-4 pr-1 py-2 f5 d-block flex-auto mr-3 color-fg-default no-underline text-bold">
|
||||
<div
|
||||
data-testid="rest-category"
|
||||
className="pl-4 pr-1 py-2 f5 d-block flex-auto mr-3 color-fg-default no-underline text-bold"
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
<span style={{ marginTop: 7 }} className="flex-shrink-0 pr-3">
|
||||
@@ -164,7 +167,7 @@ export const RestCollapsibleSection = (props: SectionProps) => {
|
||||
{/* <!-- Render the maptopic level subcategory operation links e.g. --> */}
|
||||
<ul className="list-style-none position-relative">
|
||||
{page.childPages.length <= 0 ? (
|
||||
<div data-testid="sidebar-article-group" className="pb-0">
|
||||
<div className="pb-0">
|
||||
{miniTocItems.length > 0 && (
|
||||
<ActionList
|
||||
{...{ as: 'ul' }}
|
||||
@@ -191,12 +194,7 @@ export const RestCollapsibleSection = (props: SectionProps) => {
|
||||
className="details-reset"
|
||||
>
|
||||
<summary>
|
||||
<div
|
||||
data-testid="sidebar-rest-subcategory"
|
||||
className={cx('pl-4 pr-5 py-2 no-underline')}
|
||||
>
|
||||
{childTitle}
|
||||
</div>
|
||||
<div className={cx('pl-4 pr-5 py-2 no-underline')}>{childTitle}</div>
|
||||
</summary>
|
||||
<div className="pb-0">
|
||||
{miniTocItems.length > 0 && (
|
||||
@@ -215,7 +213,7 @@ export const RestCollapsibleSection = (props: SectionProps) => {
|
||||
// We're not on the current page so don't have any minitoc
|
||||
// data so just render a link to the category page.
|
||||
return (
|
||||
<li key={childTitle} data-testid="sidebar-article-group" className="pb-0">
|
||||
<li data-testid="rest-subcategory" key={childTitle} className="pb-0">
|
||||
<Link
|
||||
href={childPage.href}
|
||||
className={cx(
|
||||
|
||||
@@ -87,7 +87,7 @@ export const SidebarProduct = () => {
|
||||
)
|
||||
return (
|
||||
<>
|
||||
<li className="my-3" data-testid="rest-sidebar-items">
|
||||
<li className="my-3">
|
||||
<ul className="list-style-none">
|
||||
{conceptualPages.map((childPage, i) => {
|
||||
const isStandaloneCategory = childPage.page.documentType === 'article'
|
||||
@@ -135,7 +135,6 @@ export const SidebarProduct = () => {
|
||||
const defaultOpen = hasExactCategory ? isActive : false
|
||||
return (
|
||||
<li
|
||||
data-testid="rest-sidebar-items"
|
||||
key={childPage.href + i}
|
||||
data-is-active-category={isActive}
|
||||
data-is-current-page={isActive && isStandaloneCategory}
|
||||
|
||||
@@ -5,43 +5,34 @@
|
||||
// Run this script to check if OpenAPI operations match versions in content/rest operations
|
||||
//
|
||||
// [end-readme]
|
||||
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { readFile, readdir } from 'fs/promises'
|
||||
import readFileAsync from '../../lib/readfile-async.js'
|
||||
import getOperations from './utils/get-operations.js'
|
||||
import frontmatter from '../../lib/read-frontmatter.js'
|
||||
import _ from 'lodash'
|
||||
import { supported } from '../../lib/enterprise-server-releases.js'
|
||||
|
||||
const supportedVersions = supported.map(Number)
|
||||
const LOWEST_SUPPORTED_GHES_VERSION = Math.min(...supportedVersions)
|
||||
const HIGHEST_SUPPORTED_GHES_VERSION = Math.max(...supportedVersions)
|
||||
import readFileAsync from '../../lib/readfile-async.js'
|
||||
import frontmatter from '../../lib/read-frontmatter.js'
|
||||
import getApplicableVersions from '../../lib/get-applicable-versions.js'
|
||||
import { allVersions } from '../../lib/all-versions.js'
|
||||
|
||||
const dereferencedPath = path.join(process.cwd(), 'lib/rest/static/dereferenced')
|
||||
const contentPath = path.join(process.cwd(), 'content/rest')
|
||||
const schemas = await readdir(dereferencedPath)
|
||||
const contentFiles = []
|
||||
const contentCheck = {}
|
||||
const openAPISchemaCheck = {}
|
||||
const dereferencedSchemas = {}
|
||||
|
||||
export async function getDiffOpenAPIContentRest() {
|
||||
const contentPath = path.join(process.cwd(), 'content/rest')
|
||||
|
||||
// Recursively go through the content/rest directory and add all categories/subcategories to contentFiles
|
||||
throughDirectory(contentPath)
|
||||
|
||||
// Add version keys to contentCheck and dereferencedSchema objects
|
||||
await addVersionKeys()
|
||||
|
||||
// Creating the categories/subcategories based on the current content directory
|
||||
await createCheckContentDirectory()
|
||||
const checkContentDir = await createCheckContentDirectory(contentFiles)
|
||||
|
||||
// Create categories/subcategories from OpenAPI Schemas
|
||||
await createOpenAPISchemasCheck()
|
||||
const openAPISchemaCheck = await createOpenAPISchemasCheck()
|
||||
|
||||
// One off edge case for secret-scanning Docs-content issue 6637
|
||||
delete openAPISchemaCheck['free-pro-team@latest']['secret-scanning']
|
||||
|
||||
// Get Differences between categories/subcategories from dereferenced schemas and the content/rest directory frontmatter versions
|
||||
const differences = getDifferences(openAPISchemaCheck, contentCheck)
|
||||
const differences = getDifferences(openAPISchemaCheck, checkContentDir)
|
||||
const errorMessages = {}
|
||||
|
||||
if (Object.keys(differences).length > 0) {
|
||||
@@ -50,108 +41,76 @@ export async function getDiffOpenAPIContentRest() {
|
||||
for (const category of differences[schemaName]) {
|
||||
if (!errorMessages[schemaName]) errorMessages[schemaName] = category
|
||||
errorMessages[schemaName][category] = {
|
||||
contentDir: contentCheck[schemaName][category],
|
||||
contentDir: checkContentDir[schemaName][category],
|
||||
openAPI: openAPISchemaCheck[schemaName][category],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errorMessages
|
||||
}
|
||||
|
||||
async function addVersionKeys() {
|
||||
for (const filename of schemas) {
|
||||
const schema = JSON.parse(await readFile(path.join(dereferencedPath, filename)))
|
||||
const key = filename.replace('.deref.json', '')
|
||||
contentCheck[key] = {}
|
||||
dereferencedSchemas[key] = schema
|
||||
}
|
||||
// GitHub Enterprise Cloud is just github.com bc it is not in the OpenAPI schema yet. Once it is, this should be updated
|
||||
contentCheck['ghec.github.com'] = {}
|
||||
dereferencedSchemas['ghec.github.com'] = dereferencedSchemas['api.github.com']
|
||||
}
|
||||
|
||||
async function createOpenAPISchemasCheck() {
|
||||
for (const [schemaName, schema] of Object.entries(dereferencedSchemas)) {
|
||||
try {
|
||||
const operationsByCategory = {}
|
||||
// munge OpenAPI definitions object in an array of operations objects
|
||||
const operations = await getOperations(schema)
|
||||
// process each operation, asynchronously rendering markdown and stuff
|
||||
await Promise.all(operations.map((operation) => operation.process()))
|
||||
const schemasPath = path.join(process.cwd(), 'lib/rest/static/decorated')
|
||||
const openAPICheck = Object.keys(allVersions).reduce((acc, val) => {
|
||||
return { ...acc, [val]: [] }
|
||||
}, {})
|
||||
// ghec does not exist in the OpenAPI yet, so we'll copy over FPT to ghec
|
||||
openAPICheck['enterprise-cloud@latest'] = []
|
||||
|
||||
// Remove any keys not needed in the decorated files
|
||||
const decoratedOperations = operations.map(
|
||||
({
|
||||
tags,
|
||||
description,
|
||||
serverUrl,
|
||||
operationId,
|
||||
categoryLabel,
|
||||
subcategoryLabel,
|
||||
contentType,
|
||||
externalDocs,
|
||||
...props
|
||||
}) => props
|
||||
)
|
||||
const schemas = fs.readdirSync(schemasPath)
|
||||
|
||||
const categories = [
|
||||
...new Set(decoratedOperations.map((operation) => operation.category)),
|
||||
].sort()
|
||||
schemas.forEach((file) => {
|
||||
const version = getVersion(file.replace('.json', ''))
|
||||
const fileData = fs.readFileSync(path.join(schemasPath, file))
|
||||
const fileSchema = JSON.parse(fileData.toString())
|
||||
const categories = Object.keys(fileSchema).sort()
|
||||
|
||||
categories.forEach((category) => {
|
||||
operationsByCategory[category] = {}
|
||||
const categoryOperations = decoratedOperations.filter(
|
||||
(operation) => operation.category === category
|
||||
)
|
||||
categoryOperations
|
||||
.filter((operation) => !operation.subcategory)
|
||||
.map((operation) => (operation.subcategory = operation.category))
|
||||
for (const category of categories) {
|
||||
const subcategories = Object.keys(fileSchema[category])
|
||||
openAPICheck[version][category] = subcategories.sort()
|
||||
|
||||
const subcategories = [
|
||||
...new Set(categoryOperations.map((operation) => operation.subcategory)),
|
||||
].sort()
|
||||
// the first item should be the item that has no subcategory
|
||||
// e.g., when the subcategory = category
|
||||
const firstItemIndex = subcategories.indexOf(category)
|
||||
if (firstItemIndex > -1) {
|
||||
const firstItem = subcategories.splice(firstItemIndex, 1)[0]
|
||||
subcategories.unshift(firstItem)
|
||||
if (version === 'free-pro-team@latest') {
|
||||
openAPICheck['enterprise-cloud@latest'][category] = [...subcategories.sort()]
|
||||
}
|
||||
}
|
||||
operationsByCategory[category] = subcategories.sort()
|
||||
})
|
||||
openAPISchemaCheck[schemaName] = operationsByCategory
|
||||
// One off edge case where secret-scanning should be removed from FPT. Docs Content #6637
|
||||
delete openAPISchemaCheck['api.github.com']['secret-scanning']
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
console.log('🐛 Whoops! Could not get operations by category!')
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
return openAPICheck
|
||||
}
|
||||
|
||||
async function createCheckContentDirectory() {
|
||||
async function createCheckContentDirectory(contentFiles) {
|
||||
const checkContent = Object.keys(allVersions).reduce((acc, val) => {
|
||||
return { ...acc, [val]: [] }
|
||||
}, {})
|
||||
|
||||
for (const filename of contentFiles) {
|
||||
const { data } = frontmatter(await readFileAsync(filename, 'utf8'))
|
||||
const applicableVersions = getApplicableVersions(data.versions, filename)
|
||||
const splitPath = filename.split('/')
|
||||
const subCategory = splitPath[splitPath.length - 1].replace('.md', '')
|
||||
const category =
|
||||
splitPath[splitPath.length - 2] === 'rest' ? subCategory : splitPath[splitPath.length - 2]
|
||||
const versions = data.versions
|
||||
|
||||
for (const version in versions) {
|
||||
const schemaNames = getSchemaName(version, versions[version])
|
||||
|
||||
for (const name of schemaNames) {
|
||||
if (!contentCheck[name][category]) {
|
||||
contentCheck[name][category] = [subCategory]
|
||||
for (const version of applicableVersions) {
|
||||
if (!checkContent[version][category]) {
|
||||
checkContent[version][category] = [subCategory]
|
||||
} else {
|
||||
contentCheck[name][category].push(subCategory)
|
||||
checkContent[version][category].push(subCategory)
|
||||
}
|
||||
contentCheck[name][category].sort()
|
||||
checkContent[version][category].sort()
|
||||
}
|
||||
}
|
||||
|
||||
return checkContent
|
||||
}
|
||||
|
||||
function getVersion(curVersion) {
|
||||
for (const version in allVersions) {
|
||||
if (Object.values(allVersions[version]).indexOf(curVersion) > -1) {
|
||||
return version
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,57 +124,6 @@ function getDifferences(openAPISchemaCheck, contentCheck) {
|
||||
return differences
|
||||
}
|
||||
|
||||
function getSchemaName(version, versionValues) {
|
||||
const versions = []
|
||||
if (version === 'fpt') {
|
||||
if (versionValues === '*') versions.push('api.github.com')
|
||||
} else if (version === 'ghec') {
|
||||
if (versionValues === '*') versions.push('ghec.github.com')
|
||||
} else if (version === 'ghae') {
|
||||
if (versionValues === '*') versions.push('github.ae')
|
||||
} else if (version === 'ghes') {
|
||||
if (versionValues === '*') {
|
||||
for (const numVer of supported) {
|
||||
versions.push('ghes-' + numVer)
|
||||
}
|
||||
} else {
|
||||
let ver = ''
|
||||
let includeVersion = false
|
||||
let goUp
|
||||
for (const char of versionValues) {
|
||||
if ((char >= '0' && char <= '9') || char === '.') {
|
||||
ver += char
|
||||
} else if (char === '=') {
|
||||
includeVersion = true
|
||||
} else if (char === '>') {
|
||||
goUp = true
|
||||
} else if (char === '<') {
|
||||
goUp = false
|
||||
}
|
||||
}
|
||||
let numVersion = parseFloat(ver).toFixed(1)
|
||||
|
||||
if (!includeVersion) {
|
||||
numVersion = goUp
|
||||
? (parseFloat(numVersion) + 0.1).toFixed(1)
|
||||
: (parseFloat(numVersion) - 0.1).toFixed(1)
|
||||
}
|
||||
|
||||
while (
|
||||
numVersion <= HIGHEST_SUPPORTED_GHES_VERSION &&
|
||||
numVersion >= LOWEST_SUPPORTED_GHES_VERSION
|
||||
) {
|
||||
numVersion = parseFloat(numVersion).toFixed(1)
|
||||
versions.push('ghes-' + numVersion)
|
||||
numVersion = goUp
|
||||
? (parseFloat(numVersion) + 0.1).toFixed(1)
|
||||
: (parseFloat(numVersion) - 0.1).toFixed(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
return versions
|
||||
}
|
||||
|
||||
function throughDirectory(directory) {
|
||||
fs.readdirSync(directory).forEach((file) => {
|
||||
const absolute = path.join(directory, file)
|
||||
|
||||
@@ -77,5 +77,7 @@ function formatErrors(differences) {
|
||||
errorMessage += '---\n'
|
||||
}
|
||||
}
|
||||
errorMessage +=
|
||||
'This means the categories and subcategories in the content/rest directory do not match the decorated files in lib/static/decorated directory from the OpenAPI schema. Please run ./script/rest/update-files.js --decorated-only and push up the file changes with your updates.\n'
|
||||
return errorMessage
|
||||
}
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { expect, jest } from '@jest/globals'
|
||||
|
||||
import '../../lib/feature-flags.js'
|
||||
import readFileAsync from '../../lib/readfile-async.js'
|
||||
import getApplicableVersions from '../../lib/get-applicable-versions.js'
|
||||
import frontmatter from '../../lib/read-frontmatter.js'
|
||||
import { getDOM } from '../helpers/e2etest.js'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { allVersions } from '../../lib/all-versions.js'
|
||||
|
||||
jest.useFakeTimers('legacy')
|
||||
|
||||
describe('sidebar', () => {
|
||||
jest.setTimeout(3 * 60 * 1000)
|
||||
@@ -14,8 +20,7 @@ describe('sidebar', () => {
|
||||
getDOM('/en'),
|
||||
getDOM('/en/github'),
|
||||
getDOM('/en/enterprise/admin'),
|
||||
// Using enterprise cloud bc we currently have a one off situation where secret-scanning is not a part of FPT
|
||||
getDOM('/en/enterprise-cloud@latest/rest'),
|
||||
getDOM('/en/rest'),
|
||||
])
|
||||
})
|
||||
|
||||
@@ -63,20 +68,82 @@ describe('sidebar', () => {
|
||||
expect($restPage('[data-testid=rest-sidebar-reference]').length).toBe(1)
|
||||
})
|
||||
|
||||
test('Check that the top level categories in the REST sidebar match content/rest directory for ghec', async () => {
|
||||
const dir = path.posix.join(process.cwd(), 'content', 'rest')
|
||||
const numCategories = []
|
||||
const sidebarRestCategories = $restPage(
|
||||
'[data-testid=sidebar] [data-testid=rest-sidebar-items] details summary div div'
|
||||
).get()
|
||||
const sidebarRestCategoryTitles = sidebarRestCategories.map((el) => $restPage(el).text().trim())
|
||||
test('Check REST categories and subcategories are rendering', async () => {
|
||||
// Get the titles from the content/rest directory to match the titles on the page
|
||||
const contentPath = path.join(process.cwd(), 'content/rest')
|
||||
const contentFiles = []
|
||||
const contentCheck = Object.keys(allVersions).reduce((acc, val) => {
|
||||
return { ...acc, [val]: { cat: [], subcat: [] } }
|
||||
}, {})
|
||||
getCatAndSubCat(contentPath)
|
||||
await createContentCheckDirectory()
|
||||
|
||||
fs.readdirSync(dir).forEach((file) => {
|
||||
if (file !== 'index.md' && file !== 'README.md' && file !== '.DS_Store') {
|
||||
numCategories.push(file)
|
||||
for (const version in allVersions) {
|
||||
// Get MapTopic level categories/subcategories for each version on /rest page
|
||||
const url = `/en/${version}/rest`
|
||||
const $ = await getDOM(url)
|
||||
|
||||
const categories = []
|
||||
$('[data-testid=sidebar] [data-testid=rest-category]').each((i, el) => {
|
||||
categories[i] = $(el).text()
|
||||
})
|
||||
|
||||
const subcategories = []
|
||||
$('[data-testid=sidebar] [data-testid=rest-subcategory] a').each((i, el) => {
|
||||
subcategories[i] = $(el).text()
|
||||
})
|
||||
|
||||
expect(contentCheck[version].cat.length).toBe(categories.length)
|
||||
expect(contentCheck[version].subcat.length).toBe(subcategories.length)
|
||||
|
||||
categories.forEach((category) => {
|
||||
expect(contentCheck[version].cat).toContain(category)
|
||||
})
|
||||
|
||||
subcategories.forEach((subcategory) => {
|
||||
expect(contentCheck[version].subcat).toContain(subcategory)
|
||||
})
|
||||
}
|
||||
// Recursively go through the content/rest directory and get all the absolute file names
|
||||
function getCatAndSubCat(directory) {
|
||||
fs.readdirSync(directory).forEach((file) => {
|
||||
const absolute = path.join(directory, file)
|
||||
if (fs.statSync(absolute).isDirectory()) {
|
||||
return getCatAndSubCat(absolute)
|
||||
} else if (
|
||||
!directory.includes('rest/guides') &&
|
||||
!directory.includes('rest/overview') &&
|
||||
!absolute.includes('rest/index.md') &&
|
||||
!file.includes('README.md')
|
||||
) {
|
||||
return contentFiles.push(absolute)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
expect(numCategories.length).toBe(sidebarRestCategoryTitles.length)
|
||||
// Create a ContentCheck object that has all the categories/subcategories and get the title from frontmatter
|
||||
async function createContentCheckDirectory() {
|
||||
for (const filename of contentFiles) {
|
||||
const { data } = frontmatter(await readFileAsync(filename, 'utf8'))
|
||||
const applicableVersions = getApplicableVersions(data.versions, filename)
|
||||
const splitPath = filename.split('/')
|
||||
let category = ''
|
||||
let subCategory = ''
|
||||
|
||||
if (splitPath[splitPath.length - 2] === 'rest') {
|
||||
category = data.title
|
||||
} else if (splitPath[splitPath.length - 3] === 'rest') {
|
||||
if (filename.includes('index.md')) {
|
||||
category = data.shortTitle || data.title
|
||||
} else {
|
||||
subCategory = data.shortTitle || data.title
|
||||
}
|
||||
}
|
||||
for (const version of applicableVersions) {
|
||||
if (category !== '') contentCheck[version].cat.push(category)
|
||||
if (subCategory !== '') contentCheck[version].subcat.push(subCategory)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user