1
0
mirror of synced 2025-12-21 19:06:49 -05:00

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:
Grace Park
2022-05-10 10:09:47 -07:00
committed by GitHub
parent 5fe80f319c
commit 2e821f52ff
5 changed files with 151 additions and 177 deletions

View File

@@ -149,7 +149,10 @@ export const RestCollapsibleSection = (props: SectionProps) => {
)} )}
> >
<div className="d-flex flex-justify-between"> <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} {title}
</div> </div>
<span style={{ marginTop: 7 }} className="flex-shrink-0 pr-3"> <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. --> */} {/* <!-- Render the maptopic level subcategory operation links e.g. --> */}
<ul className="list-style-none position-relative"> <ul className="list-style-none position-relative">
{page.childPages.length <= 0 ? ( {page.childPages.length <= 0 ? (
<div data-testid="sidebar-article-group" className="pb-0"> <div className="pb-0">
{miniTocItems.length > 0 && ( {miniTocItems.length > 0 && (
<ActionList <ActionList
{...{ as: 'ul' }} {...{ as: 'ul' }}
@@ -191,12 +194,7 @@ export const RestCollapsibleSection = (props: SectionProps) => {
className="details-reset" className="details-reset"
> >
<summary> <summary>
<div <div className={cx('pl-4 pr-5 py-2 no-underline')}>{childTitle}</div>
data-testid="sidebar-rest-subcategory"
className={cx('pl-4 pr-5 py-2 no-underline')}
>
{childTitle}
</div>
</summary> </summary>
<div className="pb-0"> <div className="pb-0">
{miniTocItems.length > 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 // We're not on the current page so don't have any minitoc
// data so just render a link to the category page. // data so just render a link to the category page.
return ( return (
<li key={childTitle} data-testid="sidebar-article-group" className="pb-0"> <li data-testid="rest-subcategory" key={childTitle} className="pb-0">
<Link <Link
href={childPage.href} href={childPage.href}
className={cx( className={cx(

View File

@@ -87,7 +87,7 @@ export const SidebarProduct = () => {
) )
return ( return (
<> <>
<li className="my-3" data-testid="rest-sidebar-items"> <li className="my-3">
<ul className="list-style-none"> <ul className="list-style-none">
{conceptualPages.map((childPage, i) => { {conceptualPages.map((childPage, i) => {
const isStandaloneCategory = childPage.page.documentType === 'article' const isStandaloneCategory = childPage.page.documentType === 'article'
@@ -135,7 +135,6 @@ export const SidebarProduct = () => {
const defaultOpen = hasExactCategory ? isActive : false const defaultOpen = hasExactCategory ? isActive : false
return ( return (
<li <li
data-testid="rest-sidebar-items"
key={childPage.href + i} key={childPage.href + i}
data-is-active-category={isActive} data-is-active-category={isActive}
data-is-current-page={isActive && isStandaloneCategory} data-is-current-page={isActive && isStandaloneCategory}

View File

@@ -5,43 +5,34 @@
// Run this script to check if OpenAPI operations match versions in content/rest operations // Run this script to check if OpenAPI operations match versions in content/rest operations
// //
// [end-readme] // [end-readme]
import fs from 'fs' import fs from 'fs'
import path from 'path' 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 _ from 'lodash'
import { supported } from '../../lib/enterprise-server-releases.js'
const supportedVersions = supported.map(Number) import readFileAsync from '../../lib/readfile-async.js'
const LOWEST_SUPPORTED_GHES_VERSION = Math.min(...supportedVersions) import frontmatter from '../../lib/read-frontmatter.js'
const HIGHEST_SUPPORTED_GHES_VERSION = Math.max(...supportedVersions) 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 contentFiles = []
const contentCheck = {}
const openAPISchemaCheck = {}
const dereferencedSchemas = {}
export async function getDiffOpenAPIContentRest() { 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 // Recursively go through the content/rest directory and add all categories/subcategories to contentFiles
throughDirectory(contentPath) throughDirectory(contentPath)
// Add version keys to contentCheck and dereferencedSchema objects
await addVersionKeys()
// Creating the categories/subcategories based on the current content directory // Creating the categories/subcategories based on the current content directory
await createCheckContentDirectory() const checkContentDir = await createCheckContentDirectory(contentFiles)
// Create categories/subcategories from OpenAPI Schemas // 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 // 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 = {} const errorMessages = {}
if (Object.keys(differences).length > 0) { if (Object.keys(differences).length > 0) {
@@ -50,108 +41,76 @@ export async function getDiffOpenAPIContentRest() {
for (const category of differences[schemaName]) { for (const category of differences[schemaName]) {
if (!errorMessages[schemaName]) errorMessages[schemaName] = category if (!errorMessages[schemaName]) errorMessages[schemaName] = category
errorMessages[schemaName][category] = { errorMessages[schemaName][category] = {
contentDir: contentCheck[schemaName][category], contentDir: checkContentDir[schemaName][category],
openAPI: openAPISchemaCheck[schemaName][category], openAPI: openAPISchemaCheck[schemaName][category],
} }
} }
} }
} }
return errorMessages 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() { async function createOpenAPISchemasCheck() {
for (const [schemaName, schema] of Object.entries(dereferencedSchemas)) { const schemasPath = path.join(process.cwd(), 'lib/rest/static/decorated')
try { const openAPICheck = Object.keys(allVersions).reduce((acc, val) => {
const operationsByCategory = {} return { ...acc, [val]: [] }
// munge OpenAPI definitions object in an array of operations objects }, {})
const operations = await getOperations(schema) // ghec does not exist in the OpenAPI yet, so we'll copy over FPT to ghec
// process each operation, asynchronously rendering markdown and stuff openAPICheck['enterprise-cloud@latest'] = []
await Promise.all(operations.map((operation) => operation.process()))
// Remove any keys not needed in the decorated files const schemas = fs.readdirSync(schemasPath)
const decoratedOperations = operations.map(
({
tags,
description,
serverUrl,
operationId,
categoryLabel,
subcategoryLabel,
contentType,
externalDocs,
...props
}) => props
)
const categories = [ schemas.forEach((file) => {
...new Set(decoratedOperations.map((operation) => operation.category)), const version = getVersion(file.replace('.json', ''))
].sort() const fileData = fs.readFileSync(path.join(schemasPath, file))
const fileSchema = JSON.parse(fileData.toString())
const categories = Object.keys(fileSchema).sort()
categories.forEach((category) => { for (const category of categories) {
operationsByCategory[category] = {} const subcategories = Object.keys(fileSchema[category])
const categoryOperations = decoratedOperations.filter( openAPICheck[version][category] = subcategories.sort()
(operation) => operation.category === category
)
categoryOperations
.filter((operation) => !operation.subcategory)
.map((operation) => (operation.subcategory = operation.category))
const subcategories = [ if (version === 'free-pro-team@latest') {
...new Set(categoryOperations.map((operation) => operation.subcategory)), openAPICheck['enterprise-cloud@latest'][category] = [...subcategories.sort()]
].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)
} }
operationsByCategory[category] = subcategories.sort()
}) })
openAPISchemaCheck[schemaName] = operationsByCategory
// One off edge case where secret-scanning should be removed from FPT. Docs Content #6637 return openAPICheck
delete openAPISchemaCheck['api.github.com']['secret-scanning']
} catch (error) {
console.error(error)
console.log('🐛 Whoops! Could not get operations by category!')
process.exit(1)
}
}
} }
async function createCheckContentDirectory() { async function createCheckContentDirectory(contentFiles) {
const checkContent = Object.keys(allVersions).reduce((acc, val) => {
return { ...acc, [val]: [] }
}, {})
for (const filename of contentFiles) { for (const filename of contentFiles) {
const { data } = frontmatter(await readFileAsync(filename, 'utf8')) const { data } = frontmatter(await readFileAsync(filename, 'utf8'))
const applicableVersions = getApplicableVersions(data.versions, filename)
const splitPath = filename.split('/') const splitPath = filename.split('/')
const subCategory = splitPath[splitPath.length - 1].replace('.md', '') const subCategory = splitPath[splitPath.length - 1].replace('.md', '')
const category = const category =
splitPath[splitPath.length - 2] === 'rest' ? subCategory : splitPath[splitPath.length - 2] splitPath[splitPath.length - 2] === 'rest' ? subCategory : splitPath[splitPath.length - 2]
const versions = data.versions
for (const version in versions) { for (const version of applicableVersions) {
const schemaNames = getSchemaName(version, versions[version]) if (!checkContent[version][category]) {
checkContent[version][category] = [subCategory]
for (const name of schemaNames) {
if (!contentCheck[name][category]) {
contentCheck[name][category] = [subCategory]
} else { } 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 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) { function throughDirectory(directory) {
fs.readdirSync(directory).forEach((file) => { fs.readdirSync(directory).forEach((file) => {
const absolute = path.join(directory, file) const absolute = path.join(directory, file)

View File

@@ -77,5 +77,7 @@ function formatErrors(differences) {
errorMessage += '---\n' 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 return errorMessage
} }

View File

@@ -1,9 +1,15 @@
import fs from 'fs'
import path from 'path'
import { expect, jest } from '@jest/globals' import { expect, jest } from '@jest/globals'
import '../../lib/feature-flags.js' 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 { getDOM } from '../helpers/e2etest.js'
import fs from 'fs' import { allVersions } from '../../lib/all-versions.js'
import path from 'path'
jest.useFakeTimers('legacy')
describe('sidebar', () => { describe('sidebar', () => {
jest.setTimeout(3 * 60 * 1000) jest.setTimeout(3 * 60 * 1000)
@@ -14,8 +20,7 @@ describe('sidebar', () => {
getDOM('/en'), getDOM('/en'),
getDOM('/en/github'), getDOM('/en/github'),
getDOM('/en/enterprise/admin'), 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/rest'),
getDOM('/en/enterprise-cloud@latest/rest'),
]) ])
}) })
@@ -63,20 +68,82 @@ describe('sidebar', () => {
expect($restPage('[data-testid=rest-sidebar-reference]').length).toBe(1) 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 () => { test('Check REST categories and subcategories are rendering', async () => {
const dir = path.posix.join(process.cwd(), 'content', 'rest') // Get the titles from the content/rest directory to match the titles on the page
const numCategories = [] const contentPath = path.join(process.cwd(), 'content/rest')
const sidebarRestCategories = $restPage( const contentFiles = []
'[data-testid=sidebar] [data-testid=rest-sidebar-items] details summary div div' const contentCheck = Object.keys(allVersions).reduce((acc, val) => {
).get() return { ...acc, [val]: { cat: [], subcat: [] } }
const sidebarRestCategoryTitles = sidebarRestCategories.map((el) => $restPage(el).text().trim()) }, {})
getCatAndSubCat(contentPath)
await createContentCheckDirectory()
fs.readdirSync(dir).forEach((file) => { for (const version in allVersions) {
if (file !== 'index.md' && file !== 'README.md' && file !== '.DS_Store') { // Get MapTopic level categories/subcategories for each version on /rest page
numCategories.push(file) 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)
}
}
}
}) })
}) })