* Copyright (c) 2026 Huawei Technologies Co., Ltd.
* This program is free software, you can redistribute it and/or modify it under the terms and conditions of
* CANN Open Software License Agreement Version 2.0 (the "License").
* Please refer to the License for details. You may not use this file except in compliance with the License.
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
* See LICENSE in the root of the software repository for the full text of the License.
*/
import assert from 'node:assert/strict'
import test from 'node:test'
import {
extractUnsupportedProducts,
filterNavigationCandidates,
filterSidebarItems,
getFilteredSidebarConfig,
getProductFilterValue,
isApiRouteUnsupported,
normalizeApiRoute,
renumberSidebarItems,
} from '../api-support.mjs'
test('maps all supported product labels to existing filter values', () => {
const labels = {
'Ascend 950PR/Ascend 950DT': '950',
'Atlas A3 训练系列产品/Atlas A3 推理系列产品': 'A3',
'Atlas A2 训练系列产品/Atlas A2 推理系列产品': '910b',
'Atlas 200I/500 A2 推理产品': '310b',
'Atlas 推理系列产品AI Core': '310p',
'Atlas 推理系列产品 AI Core': '310p',
'Atlas 推理系列产品Vector Core': '310p',
'Atlas 推理系列产品 Vector Core': '310p',
'Atlas 训练系列产品': '910',
'Kirin X90': 'x90',
'Kirin 9030': '9030',
}
for (const [label, expected] of Object.entries(labels)) {
assert.equal(getProductFilterValue(label), expected)
}
})
test('treats a product family as supported when any member is supported', () => {
const markdown = `# Abs
## 产品支持情况<a name="support"></a>
- Atlas 推理系列产品AI Core:支持
- Atlas 推理系列产品Vector Core:不支持
## 功能说明
- Atlas 推理系列产品AI Core:不支持
`
assert.deepEqual(extractUnsupportedProducts(markdown), [])
})
test('marks a product family unsupported only when every member is unsupported', () => {
const markdown = `## 产品支持情况
- Ascend 950PR/Ascend 950DT:支持
- Atlas 推理系列产品 AI Core:不支持
- Atlas 推理系列产品 Vector Core:不支持
- Kirin X90:不支持
`
assert.deepEqual(extractUnsupportedProducts(markdown), ['310p', 'x90'])
})
test('defaults to visible when support information is missing or unrecognized', () => {
assert.deepEqual(extractUnsupportedProducts('# Page\n\n## 功能说明\n正文'), [])
assert.deepEqual(
extractUnsupportedProducts('## 产品支持情况\n- 未知产品:不支持\n'),
[]
)
})
test('normalizes Markdown, HTML, encoded, and query-string routes', () => {
assert.equal(normalizeApiRoute('api/目录/Abs.md'), '/api/目录/Abs')
assert.equal(normalizeApiRoute('/api/%E7%9B%AE%E5%BD%95/Abs.html?q=1#top'), '/api/目录/Abs')
})
test('checks explicit unsupported routes and keeps unknown routes visible', () => {
const index = { '/api/目录/Abs': ['310p'] }
assert.equal(isApiRouteUnsupported(index, '/api/目录/Abs.html', '310p'), true)
assert.equal(isApiRouteUnsupported(index, '/api/目录/Abs', '950'), false)
assert.equal(isApiRouteUnsupported(index, '/api/目录/Unknown', '310p'), false)
assert.equal(isApiRouteUnsupported(index, '/api/目录/Abs', 'all'), false)
})
test('filters unsupported leaves, empty groups, and unsupported parent links', () => {
const source = [
{
text: 'Group',
link: '/api/group',
items: [
{ text: 'Hidden', link: '/api/hidden' },
{ text: 'Visible', link: '/api/visible' },
],
},
{ text: 'Empty group', items: [{ text: 'Only hidden', link: '/api/only-hidden' }] },
{ text: 'Unknown', link: '/api/unknown' },
]
const index = {
'/api/group': ['310p'],
'/api/hidden': ['310p'],
'/api/only-hidden': ['310p'],
}
const filtered = filterSidebarItems(source, '310p', index)
assert.deepEqual(filtered, [
{ text: 'Group', items: [{ text: 'Visible', link: '/api/visible' }] },
{ text: 'Unknown', link: '/api/unknown' },
])
assert.equal(source[0].link, '/api/group')
assert.equal(source[0].items.length, 2)
})
test('returns a cloned complete sidebar for the all filter', () => {
const source = [{ text: 'Page', link: '/api/page' }]
const filtered = filterSidebarItems(source, 'all', { '/api/page': ['310p'] })
assert.deepEqual(filtered, source)
assert.notEqual(filtered, source)
assert.notEqual(filtered[0], source[0])
})
test('builds an API sidebar config for the selected product without mutating the source', () => {
const apiSidebarSource = [
{ text: 'Hidden', link: '/api/hidden' },
{ text: 'Visible', link: '/api/visible' },
]
const theme = {
sidebar: {
'/guide/': [{ text: 'Guide', link: '/guide/start' }],
'/api/': apiSidebarSource,
},
apiUnsupportedIndex: { '/api/hidden': ['310p'] },
}
const filtered = getFilteredSidebarConfig(theme, '310p')
assert.deepEqual(filtered['/api/'], [{
text: '1. Visible',
link: '/api/visible',
unnumberedText: 'Visible',
}])
assert.equal(filtered['/guide/'], theme.sidebar['/guide/'])
assert.equal(theme.sidebar['/api/'].length, 2)
})
test('renumbers every visible sidebar level after product filtering', () => {
const source = [{
text: '2.2.9. Reg vector',
unnumberedText: 'Reg vector',
items: [
{ text: '2.2.9.1. Overview', unnumberedText: 'Overview', link: '/api/reg/overview' },
{ text: '2.2.9.2. Hidden one', unnumberedText: 'Hidden one', link: '/api/reg/hidden-one' },
{ text: '2.2.9.3. Types', unnumberedText: 'Types', link: '/api/reg/types' },
{ text: '2.2.9.4. Hidden two', unnumberedText: 'Hidden two', link: '/api/reg/hidden-two' },
{ text: '2.2.9.5. Features', unnumberedText: 'Features', link: '/api/reg/features' },
],
}]
const index = {
'/api/reg/hidden-one': ['A3'],
'/api/reg/hidden-two': ['A3'],
}
const filtered = filterSidebarItems(source, 'A3', index)
const renumbered = renumberSidebarItems(filtered)
assert.deepEqual(renumbered[0].items.map(item => item.text), [
'1.1. Overview',
'1.2. Types',
'1.3. Features',
])
})
test('uses the untouched original sidebar config for the all filter', () => {
const apiSidebarSource = [{
text: '2.2.9. Reg vector',
unnumberedText: 'Reg vector',
link: '/api/reg/reg',
}]
const theme = {
sidebar: { '/api/': apiSidebarSource },
apiSidebarSource,
apiUnsupportedIndex: { '/api/reg/reg': ['A3'] },
}
const filtered = getFilteredSidebarConfig(theme, 'all')
assert.equal(filtered, theme.sidebar)
assert.equal(filtered['/api/'], apiSidebarSource)
assert.equal(filtered['/api/'][0].text, '2.2.9. Reg vector')
})
test('keeps the current page as a prev-next anchor while skipping unsupported neighbors', () => {
const candidates = [
{ text: 'Previous', link: '/api/previous' },
{ text: 'Hidden previous', link: '/api/hidden-previous' },
{ text: 'Current', link: '/api/current' },
{ text: 'Hidden next', link: '/api/hidden-next' },
{ text: 'Next', link: '/api/next' },
]
const index = {
'/api/hidden-previous': ['310p'],
'/api/current': ['310p'],
'/api/hidden-next': ['310p'],
}
const filtered = filterNavigationCandidates(
candidates,
'api/current.md',
'310p',
index
)
assert.deepEqual(filtered.map(item => item.text), ['Previous', 'Current', 'Next'])
})
test('hides a group when product filtering leaves only its overview page', () => {
const source = [{
text: 'Logic',
link: '/api/logic/logic',
items: [
{ text: 'Overview', link: '/api/logic/overview' },
{ text: 'And', link: '/api/logic/and' },
{ text: 'Or', link: '/api/logic/or' },
],
}]
const index = {
'/api/logic/and': ['A3'],
'/api/logic/or': ['A3'],
}
assert.deepEqual(filterSidebarItems(source, 'A3', index), [])
})
test('hides a group when all interfaces disappear even if its own page remains supported', () => {
const source = [{
text: 'Reduce',
link: '/api/reduce/reduce',
items: [
{ text: 'ReduceOne', link: '/api/reduce/reduce-one' },
{ text: 'ReduceTwo', link: '/api/reduce/reduce-two' },
],
}]
const index = {
'/api/reduce/reduce-one': ['A3'],
'/api/reduce/reduce-two': ['A3'],
}
assert.deepEqual(filterSidebarItems(source, 'A3', index), [])
})
test('keeps overview and same-name-only groups that were structural before filtering', () => {
const source = [
{
text: 'Overview only',
link: '/api/overview-only/overview-only',
items: [{ text: 'Overview', link: '/api/overview-only/overview' }],
},
{
text: 'Self page only',
items: [{ text: 'Self page', link: '/api/self-page-only/self-page-only' }],
},
]
assert.deepEqual(filterSidebarItems(source, 'A3', {}), source)
})
test('keeps a group and its overview when at least one interface supports the product', () => {
const source = [{
text: 'Logic',
link: '/api/logic/logic',
items: [
{ text: 'Overview', link: '/api/logic/introduction' },
{ text: 'Hidden', link: '/api/logic/hidden' },
{ text: 'Visible', link: '/api/logic/visible' },
],
}]
const index = { '/api/logic/hidden': ['A3'] }
assert.deepEqual(filterSidebarItems(source, 'A3', index), [{
text: 'Logic',
link: '/api/logic/logic',
items: [
{ text: 'Overview', link: '/api/logic/introduction' },
{ text: 'Visible', link: '/api/logic/visible' },
],
}])
})
test('hides parent groups after their interface-bearing child groups disappear', () => {
const source = [{
text: 'Parent',
items: [{
text: 'Logic',
link: '/api/parent/logic/logic',
items: [
{ text: 'Overview', link: '/api/parent/logic/overview' },
{ text: 'And', link: '/api/parent/logic/and' },
],
}],
}]
const index = { '/api/parent/logic/and': ['A3'] }
assert.deepEqual(filterSidebarItems(source, 'A3', index), [])
})
test('keeps an intrinsic documentation subgroup when sibling interfaces disappear', () => {
const source = [{
text: 'Parent',
link: '/api/parent/parent',
items: [
{
text: 'Documentation',
link: '/api/parent/documentation/documentation',
items: [{ text: 'Overview', link: '/api/parent/documentation/overview' }],
},
{ text: 'Hidden interface', link: '/api/parent/hidden-interface' },
],
}]
const index = { '/api/parent/hidden-interface': ['A3'] }
assert.deepEqual(filterSidebarItems(source, 'A3', index), [{
text: 'Parent',
link: '/api/parent/parent',
items: [{
text: 'Documentation',
link: '/api/parent/documentation/documentation',
items: [{ text: 'Overview', link: '/api/parent/documentation/overview' }],
}],
}])
})
test('keeps the complete original group structure for the all filter', () => {
const source = [{
text: 'Logic',
link: '/api/logic/logic',
items: [
{ text: 'Overview', link: '/api/logic/overview' },
{ text: 'And', link: '/api/logic/and' },
],
}]
assert.deepEqual(
filterSidebarItems(source, 'all', { '/api/logic/and': ['A3'] }),
source
)
})
test('prev-next candidates follow the filtered tree and skip hidden structural pages', () => {
const candidates = [
{ text: 'Previous', link: '/api/previous' },
{ text: 'Logic', link: '/api/logic/logic' },
{ text: 'Overview', link: '/api/logic/overview' },
{ text: 'Current', link: '/api/logic/current' },
{ text: 'Next', link: '/api/next' },
]
const filteredApiSidebar = [
{ text: 'Previous', link: '/api/previous' },
{ text: 'Next', link: '/api/next' },
]
const filtered = filterNavigationCandidates(
candidates,
'api/logic/current.md',
'A3',
{ '/api/logic/current': ['A3'] },
filteredApiSidebar
)
assert.deepEqual(filtered.map(item => item.text), ['Previous', 'Current', 'Next'])
})
test('prev-next candidates use titles from the renumbered visible tree', () => {
const candidates = [
{ text: '2. Previous', link: '/api/previous' },
{ text: '5. Next', link: '/api/next' },
]
const filteredApiSidebar = [
{ text: '1. Previous', link: '/api/previous' },
{ text: '2. Next', link: '/api/next' },
]
const filtered = filterNavigationCandidates(
candidates,
'/api/previous',
'A3',
{},
filteredApiSidebar
)
assert.deepEqual(filtered.map(item => item.text), ['1. Previous', '2. Next'])
})