feat(sidebar): normalize page links and control root chevrons (#2790)

This commit is contained in:
Luffy
2026-09-04 10:50:37 +08:00
committed by GitHub
parent e5ccdfb24e
commit 84c2d67813
10 changed files with 443 additions and 56 deletions
+1
View File
@@ -34,6 +34,7 @@ const d = new Docsify({
themeColor: 'deeppink',
hideSidebar: false,
collapseSidebarGroups: true,
collapsibleSidebarGroups: true,
// @ts-expect-error invalid property to test that type checking works
blahblah: 123,
+315 -8
View File
@@ -69,8 +69,56 @@ test.describe('Sidebar Tests', () => {
expect(page.url()).toMatch(/\/test%3Efoo$/);
});
test('collapses root sidebar groups', async ({ page }) => {
test('does not collapse root sidebar groups by default', async ({ page }) => {
await docsifyInit({
config: {
collapseSidebarGroups: true,
},
styleURLs: ['/dist/themes/core.css'],
html: `
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8" /></head>
<body class="sidebar-chevron-right">
<div id="app"></div>
</body>
</html>
`,
markdown: {
sidebar: `
- Getting started
- [Quick start](quickstart)
`,
},
routes: {
'/quickstart.md': '# Quick start',
},
});
const group = page.locator('.sidebar-nav > ul > li.group');
const groupTitle = group.locator(':scope > p');
const childLink = group.locator(':scope > ul > li > a');
await expect(group).not.toHaveClass(/collapse/);
await expect(groupTitle).not.toHaveClass(/group-toggle/);
await expect(groupTitle).not.toHaveAttribute('role');
await expect(groupTitle).not.toHaveAttribute('tabindex');
await expect(groupTitle).not.toHaveAttribute('aria-expanded');
await expect(groupTitle).not.toHaveAttribute('data-group-id');
await expect(groupTitle).toHaveCSS('background-image', 'none');
await expect(groupTitle).toHaveCSS('cursor', 'auto');
await expect(childLink).toBeVisible();
await groupTitle.click();
await expect(group).not.toHaveClass(/collapse/);
await expect(childLink).toBeVisible();
});
test('collapses root sidebar groups when configured', async ({ page }) => {
await docsifyInit({
config: {
collapsibleSidebarGroups: true,
},
styleURLs: ['/dist/themes/core.css'],
markdown: {
sidebar: `
@@ -92,7 +140,7 @@ test.describe('Sidebar Tests', () => {
});
const group = page.locator('.sidebar-nav > ul > li').first();
const groupTitle = group.locator(':scope > p.group-title');
const groupTitle = group.locator(':scope > p.group-toggle');
const childLink = group.locator(':scope > ul > li > a');
await expect(groupTitle).toHaveAttribute('role', 'button');
@@ -132,12 +180,96 @@ test.describe('Sidebar Tests', () => {
await expect(group).not.toHaveClass(/collapse/);
});
for (const viewport of [
{
name: 'desktop',
width: 1280,
height: 720,
prepareSidebar: async () => {},
},
{
name: 'mobile',
width: 390,
height: 500,
prepareSidebar: async (page, sidebar) => {
await page.locator('.sidebar-toggle-button').click();
await sidebar.evaluate(element =>
Promise.all(
element.getAnimations().map(animation => animation.finished),
),
);
},
},
]) {
test(`keeps sidebar content aligned when a root group removes the scrollbar on ${viewport.name}`, async ({
page,
}) => {
await page.setViewportSize(viewport);
const childLinks = Array.from(
{ length: 40 },
(_, index) => ` - [Page ${index + 1}](page-${index + 1})`,
).join('\n');
await docsifyInit({
config: {
collapsibleSidebarGroups: true,
},
styleURLs: ['/dist/themes/core.css'],
markdown: {
homepage: '# Home',
sidebar: `- Root\n${childLinks}`,
},
});
const sidebar = page.locator('.sidebar');
const group = page.locator('.sidebar-nav > ul > li.group');
const groupTitle = group.locator(':scope > .group-toggle');
await viewport.prepareSidebar(page, sidebar);
await expect(sidebar).toHaveCSS('overflow-y', 'scroll');
const expandedLayout = await page.evaluate(() => {
const sidebar = document.querySelector('.sidebar');
const groupTitle = document.querySelector('.group-toggle');
return {
hasScrollbar: sidebar.scrollHeight > sidebar.clientHeight,
titleRight: groupTitle.getBoundingClientRect().right,
url: location.href,
};
});
expect(expandedLayout.hasScrollbar).toBe(true);
await groupTitle.click();
await expect(group).toHaveClass(/collapse/);
const collapsedLayout = await page.evaluate(() => {
const sidebar = document.querySelector('.sidebar');
const groupTitle = document.querySelector('.group-toggle');
return {
hasScrollbar: sidebar.scrollHeight > sidebar.clientHeight,
titleRight: groupTitle.getBoundingClientRect().right,
url: location.href,
};
});
expect(collapsedLayout.hasScrollbar).toBe(false);
expect(collapsedLayout.titleRight).toBe(expandedLayout.titleRight);
expect(collapsedLayout.url).toBe(expandedLayout.url);
});
}
test('initially collapses root sidebar groups when configured', async ({
page,
}) => {
await docsifyInit({
config: {
collapseSidebarGroups: true,
collapsibleSidebarGroups: true,
},
styleURLs: ['/dist/themes/core.css'],
markdown: {
@@ -158,7 +290,7 @@ test.describe('Sidebar Tests', () => {
const groups = page.locator('.sidebar-nav > ul > li.group');
const firstGroup = groups.first();
const firstGroupTitle = firstGroup.locator(':scope > .group-title');
const firstGroupTitle = firstGroup.locator(':scope > .group-toggle');
const firstGroupLink = firstGroup.locator(':scope > ul > li > a');
const secondGroup = groups.nth(1);
@@ -180,11 +312,15 @@ test.describe('Sidebar Tests', () => {
test('supports chevrons on collapsible root groups', async ({ page }) => {
await docsifyInit({
config: {
collapsibleSidebarGroups: true,
},
styleURLs: ['/dist/themes/core.css'],
style: `
:root:has(body[class*='sidebar-chevron']) {
--sidebar-chevron-collapsed-color: rgb(1, 2, 3);
--sidebar-chevron-expanded-color: rgb(4, 5, 6);
--sidebar-group-title-font-weight: 700;
}
`,
html: `
@@ -201,15 +337,25 @@ test.describe('Sidebar Tests', () => {
- Getting started
- [Quick start](quickstart)
- [Standalone](standalone)
1. Styled group
- [Styled child](styled-child)
`,
},
routes: {
'/quickstart.md': '# Quick start',
'/styled-child.md': '# Styled child',
'/standalone.md': '# Standalone',
},
});
const groupTitle = page.locator('.group-title[role="button"]');
const groupTitle = page
.locator('.group-toggle[role="button"]')
.filter({ hasText: 'Getting started' });
const styledGroupTitle = page
.locator('.group-title.group-toggle')
.filter({ hasText: 'Styled group' });
const standaloneLink = page.locator('a[href="#/standalone"]');
const background = await groupTitle.evaluate(
element => getComputedStyle(element).backgroundImage,
@@ -218,10 +364,18 @@ test.describe('Sidebar Tests', () => {
groupTitle.boundingBox(),
standaloneLink.boundingBox(),
]);
const [groupTitleFontWeight, standaloneLinkFontWeight] = await Promise.all([
groupTitle.evaluate(element => getComputedStyle(element).fontWeight),
standaloneLink.evaluate(element => getComputedStyle(element).fontWeight),
]);
expect(background).not.toBe('none');
expect(background).toMatch(/rgb\(1,\s*2,\s*3\)/);
expect(background).not.toMatch(/rgb\(4,\s*5,\s*6\)/);
await expect(groupTitle).not.toHaveClass(/group-title/);
expect(groupTitleFontWeight).toBe(standaloneLinkFontWeight);
await expect(styledGroupTitle).not.toHaveCSS('background-image', 'none');
await expect(styledGroupTitle).toHaveCSS('font-weight', '700');
expect(groupTitleBox?.x + groupTitleBox?.width).toBe(
standaloneLinkBox?.x + standaloneLinkBox?.width,
);
@@ -236,10 +390,163 @@ test.describe('Sidebar Tests', () => {
expect(collapsedBackground).not.toMatch(/rgb\(4,\s*5,\s*6\)/);
});
test('normalizes loose-list page links and shows expanded chevrons', async ({
page,
}) => {
await docsifyInit({
config: {
subMaxLevel: 2,
},
styleURLs: ['/dist/themes/core.css'],
style: `
:root:has(body[class*='sidebar-chevron']) {
--sidebar-chevron-collapsed-color: rgb(1, 2, 3);
--sidebar-chevron-expanded-color: rgb(4, 5, 6);
--sidebar-link-color-active: rgb(7, 8, 9);
}
`,
html: `
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8" /></head>
<body class="sidebar-chevron-right">
<div id="app"></div>
</body>
</html>
`,
markdown: {
homepage: '# Home',
sidebar: `
* [Test](test.md)
+ [Quick start](quickstart.md)
- [Adding pages](adding-pages.md)
- Getting started
- [Cover page](cover.md)
`,
},
routes: {
'/test.md': '# Test',
'/quickstart.md': '# Quick start\n\n## Installation',
'/adding-pages.md': '# Adding pages\n\n## Sidebar',
'/cover.md': '# Cover page',
},
});
const quickStartLink = page.locator('a[href="#/quickstart"]');
const addingPagesLink = page.locator('a[href="#/adding-pages"]');
const quickStartItem = page.locator(
'.sidebar-nav li:has(> a[href="#/quickstart"])',
);
const addingPagesItem = page.locator(
'.sidebar-nav li:has(> a[href="#/adding-pages"])',
);
await expect(page.locator('.sidebar-nav li > p > a')).toHaveCount(0);
await quickStartLink.click();
await expect(
quickStartItem.locator(':scope > .app-sub-sidebar'),
).toBeVisible();
const quickStartBackground = await quickStartLink.evaluate(
element => getComputedStyle(element).backgroundImage,
);
await addingPagesLink.click();
await expect(
addingPagesItem.locator(':scope > .app-sub-sidebar'),
).toBeVisible();
const addingPagesBackground = await addingPagesLink.evaluate(
element => getComputedStyle(element).backgroundImage,
);
expect(addingPagesBackground).toBe(quickStartBackground);
expect(addingPagesBackground).toMatch(/rgb\(4,\s*5,\s*6\)/);
await expect(addingPagesLink).toHaveCSS('color', 'rgb(7, 8, 9)');
await addingPagesLink.click();
await expect(addingPagesItem).toHaveClass(/collapse/);
const collapsedBackground = await addingPagesLink.evaluate(
element => getComputedStyle(element).backgroundImage,
);
expect(collapsedBackground).not.toBe(addingPagesBackground);
});
test('hides root chevrons when configured by body class', async ({
page,
}) => {
await docsifyInit({
config: {
collapsibleSidebarGroups: true,
subMaxLevel: 2,
},
styleURLs: ['/dist/themes/core.css'],
html: `
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8" /></head>
<body class="sidebar-chevron-right sidebar-chevron-root-hidden">
<div id="app"></div>
</body>
</html>
`,
markdown: {
homepage: '# Home',
sidebar: `
+ [Direct root page](direct.md)
- [Loose root page](loose.md)
- Getting started
- [Nested page](nested.md)
`,
},
routes: {
'/direct.md': '# Direct root page',
'/loose.md': '# Loose root page\n\n## Child heading',
'/nested.md': '# Nested page',
},
});
const directRootLink = page.locator('a[href="#/direct"]');
const looseRootLink = page.locator('a[href="#/loose"]');
const nestedLink = page.locator('a[href="#/nested"]');
const groupTitle = page.locator('.group-toggle[role="button"]');
const looseRootItem = page.locator(
'.sidebar-nav li:has(> a[href="#/loose"])',
);
for (const rootLink of [directRootLink, looseRootLink]) {
await expect(rootLink).toHaveCSS('background-image', 'none');
}
await expect(nestedLink).not.toHaveCSS('background-image', 'none');
await expect(groupTitle).toHaveCSS('background-image', 'none');
await groupTitle.click();
await expect(groupTitle).toHaveAttribute('aria-expanded', 'false');
await expect(groupTitle).toHaveCSS('background-image', 'none');
await looseRootLink.click();
await expect(
looseRootItem.locator(':scope > .app-sub-sidebar'),
).toBeVisible();
await expect(looseRootLink).toHaveCSS('background-image', 'none');
await looseRootLink.click();
await expect(looseRootItem).toHaveClass(/collapse/);
await expect(looseRootLink).toHaveCSS('background-image', 'none');
});
test('keeps group border spacing when the last group collapses', async ({
page,
}) => {
await docsifyInit({
config: {
collapsibleSidebarGroups: true,
},
styleURLs: ['/dist/themes/core.css'],
html: `
<!DOCTYPE html>
@@ -267,12 +574,12 @@ test.describe('Sidebar Tests', () => {
const upgradingGroup = page.locator(
'.sidebar-nav > ul:first-of-type > li:last-child',
);
const groupTitle = upgradingGroup.locator(':scope > .group-title');
const groupTitle = upgradingGroup.locator(':scope > .group-toggle');
await groupTitle.click();
const spacing = await page.evaluate(() => {
const title = document.querySelector('.group-title');
const title = document.querySelector('.group-toggle');
const group = title.closest('li');
const awesome = document.querySelector('a[href="#/awesome"]');
const titleBox = title.getBoundingClientRect();
@@ -288,7 +595,7 @@ test.describe('Sidebar Tests', () => {
expect(spacing.titleToBorder).toBeGreaterThan(spacing.borderToAwesome);
});
test('keeps a loose-list page link visible when collapsed', async ({
test('keeps a normalized loose-list page link visible when collapsed', async ({
page,
}) => {
await docsifyInit({
@@ -320,7 +627,7 @@ test.describe('Sidebar Tests', () => {
await quickStartLink.click();
const quickStartItem = page.locator(
'.sidebar-nav li:has(> p > a[href="#/quickstart"])',
'.sidebar-nav li:has(> a[href="#/quickstart"])',
);
const subSidebar = quickStartItem.locator(':scope > .app-sub-sidebar');
await expect(subSidebar).toBeVisible();