diff --git a/e2e/fixtures/auto-nav-sidebar-dir-convention/index.test.ts b/e2e/fixtures/auto-nav-sidebar-dir-convention/index.test.ts index d6627d472..b3c12d7f1 100644 --- a/e2e/fixtures/auto-nav-sidebar-dir-convention/index.test.ts +++ b/e2e/fixtures/auto-nav-sidebar-dir-convention/index.test.ts @@ -3,8 +3,8 @@ import { getSidebar, getSidebarTexts } from '../../utils/getSideBar'; import { getPort, killProcess, runDevCommand } from '../../utils/runCommands'; test.describe('Auto nav and sidebar dir convention', async () => { - let appPort; - let app; + let appPort: number; + let app: Awaited>; test.beforeAll(async () => { const appDir = __dirname; appPort = await getPort(); @@ -25,14 +25,15 @@ test.describe('Auto nav and sidebar dir convention', async () => { }); const sidebarTexts = await getSidebarTexts(page); - expect(sidebarTexts.length).toBe(7); + expect(sidebarTexts.length).toBe(8); expect(sidebarTexts.join(',')).toEqual( [ '/guide Page', 'index md convention', 'index mdx convention', 'same name', - 'index in metaIndex in meta', + 'index in meta', + 'Index in meta', 'no meta md', 'no meta mdx', ].join(','), @@ -45,7 +46,11 @@ test.describe('Auto nav and sidebar dir convention', async () => { await page.goto(`http://localhost:${appPort}/guide/`, { waitUntil: 'networkidle', }); - await page.click('.rspress-scrollbar nav section div'); + await page + .locator( + '.rp-doc-layout__sidebar .rp-sidebar-item[data-context="context-index-md-convention"]', + ) + .click(); await page.waitForURL('**/index-md-convention/**'); expect(page.url()).toBe( `http://localhost:${appPort}/guide/index-md-convention/index.html`, @@ -56,71 +61,47 @@ test.describe('Auto nav and sidebar dir convention', async () => { await page.goto(`http://localhost:${appPort}/guide/`, { waitUntil: 'networkidle', }); + const itemsWithContext = await page + .locator('.rp-doc-layout__sidebar .rp-sidebar-item[data-context]') + .evaluateAll(sidebarNodes => + sidebarNodes + .map(node => node.getAttribute('data-context')) + .filter((context): context is string => Boolean(context)), + ); + expect(itemsWithContext).toEqual([ + 'context-index-md-convention', + 'context-index-mdx-convention', + 'context-same-name', + 'context-index-in-meta', + 'context-no-meta-md', + 'context-no-meta-mdx', + ]); - const sidebarGroupSections = await page.$$('.rspress-sidebar-section'); - - // first level - const contexts1 = await page.evaluate( - sidebars => - sidebars?.map(sidebar => sidebar.getAttribute('data-context')), - sidebarGroupSections, - ); - expect(contexts1.join(',')).toEqual( - [ - 'context-index-md-convention', - 'context-index-mdx-convention', - 'context-same-name', - '', - 'context-no-meta-md', - 'context-no-meta-mdx', - ].join(','), - ); - - const sidebarGroupCollapses = await page.$$('.rspress-sidebar-collapse'); - const contexts2 = await page.evaluate( - sidebars => - sidebars?.map(sidebar => sidebar.getAttribute('data-context')), - sidebarGroupCollapses, - ); - expect(contexts2.join(',')).toEqual( - [ - 'context-index-md-convention', - 'context-index-mdx-convention', - 'context-same-name', - '', - 'context-no-meta-md', - 'context-no-meta-mdx', - ].join(','), - ); - - const sidebarGroupItems = await page.$$('.rspress-sidebar-item'); - const contexts3 = await page.evaluate( - sidebarGroupConfig => - sidebarGroupConfig?.map(sidebarItem => - sidebarItem.getAttribute('data-context'), - ), - sidebarGroupItems, - ); - // added container `div.rspress-sidebar-item` to ( depth=0 & type=file )'s sidebar item - // so have to modify this test result - expect(contexts3.join(',')).toEqual( - ['', 'context-index-in-meta'].join(','), - ); + const nestedContexts = await page + .locator( + '.rp-doc-layout__sidebar .rp-sidebar-item[data-depth="1"][data-context]', + ) + .evaluateAll(sidebarNodes => + sidebarNodes + .map(node => node.getAttribute('data-context')) + .filter((context): context is string => Boolean(context)), + ); + expect(nestedContexts).toEqual(['context-index-in-meta']); }); test('/api/config/index.html /api/config/index /api/config should be the same page', async ({ page, }) => { async function getSidebarLength(): Promise { - return ((await getSidebar(page)) ?? []).length; + return getSidebar(page).count(); } async function isMenuItemActive(): Promise { - const activeMenuItem = await page.$( - '.rspress-sidebar-collapse[class*="menuItemActive"]', + const activeMenuItem = page.locator( + '.rp-doc-layout__sidebar .rp-sidebar-item--active .rp-sidebar-item__left span', ); - const content = await activeMenuItem?.textContent(); - return content === 'index md convention'; + const content = await activeMenuItem.textContent(); + return content?.trim() === 'index md convention'; } // /api/config/index.html await page.goto( @@ -129,7 +110,7 @@ test.describe('Auto nav and sidebar dir convention', async () => { waitUntil: 'networkidle', }, ); - expect(await getSidebarLength()).toBe(7); + expect(await getSidebarLength()).toBe(8); expect(await isMenuItemActive()).toBe(true); // /api/config/index @@ -139,14 +120,14 @@ test.describe('Auto nav and sidebar dir convention', async () => { waitUntil: 'networkidle', }, ); - expect(await getSidebarLength()).toBe(7); + expect(await getSidebarLength()).toBe(8); expect(await isMenuItemActive()).toBe(true); // /api/config await page.goto(`http://localhost:${appPort}/guide/index-md-convention`, { waitUntil: 'networkidle', }); - expect(await getSidebarLength()).toBe(7); + expect(await getSidebarLength()).toBe(8); expect(await isMenuItemActive()).toBe(true); }); }); diff --git a/e2e/fixtures/auto-nav-sidebar-issue-1682/index.test.ts b/e2e/fixtures/auto-nav-sidebar-issue-1682/index.test.ts index 6a6f6e536..a006fe751 100644 --- a/e2e/fixtures/auto-nav-sidebar-issue-1682/index.test.ts +++ b/e2e/fixtures/auto-nav-sidebar-issue-1682/index.test.ts @@ -3,8 +3,8 @@ import { getSidebarTexts } from '../../utils/getSideBar'; import { getPort, killProcess, runDevCommand } from '../../utils/runCommands'; test.describe('Auto nav and sidebar dir issue-1682', async () => { - let appPort; - let app; + let appPort: number; + let app: Awaited>; test.beforeAll(async () => { const appDir = __dirname; appPort = await getPort(); @@ -25,9 +25,9 @@ test.describe('Auto nav and sidebar dir issue-1682', async () => { }); const sidebarTexts = await getSidebarTexts(page); - expect(sidebarTexts.length).toBe(2); + expect(sidebarTexts.length).toBe(3); expect(sidebarTexts.join(',')).toEqual( - ['Second sub-directorytest', 'First sub-directory'].join(','), + ['Second sub-directory', 'test', 'First sub-directory'].join(','), ); }); }); diff --git a/e2e/fixtures/auto-nav-sidebar-no-meta/index.test.ts b/e2e/fixtures/auto-nav-sidebar-no-meta/index.test.ts index 39e2b411a..bdf7e47a6 100644 --- a/e2e/fixtures/auto-nav-sidebar-no-meta/index.test.ts +++ b/e2e/fixtures/auto-nav-sidebar-no-meta/index.test.ts @@ -3,8 +3,8 @@ import { getSidebarTexts } from '../../utils/getSideBar'; import { getPort, killProcess, runDevCommand } from '../../utils/runCommands'; test.describe('Auto nav and sidebar test', async () => { - let appPort; - let app; + let appPort: number; + let app: Awaited>; test.beforeAll(async () => { const appDir = __dirname; appPort = await getPort(); @@ -23,16 +23,19 @@ test.describe('Auto nav and sidebar test', async () => { }); const sidebarTexts = await getSidebarTexts(page); - expect(sidebarTexts.length).toBe(2); - expect(sidebarTexts.join(',')).toEqual( - [ - 'API', - 'pluginPlugin aPlugin b', - 'Commands', - 'configBasic configBuild configFront matter configTheme config,', - 'HomePage', - ].join(''), - ); + expect(sidebarTexts).toEqual([ + 'API', + 'plugin', + 'Plugin a', + 'Plugin b', + 'Commands', + 'config', + 'Basic config', + 'Build config', + 'Front matter config', + 'Theme config', + 'HomePage', + ]); }); test('Should click the directory and navigate to the index page', async ({ @@ -42,13 +45,16 @@ test.describe('Auto nav and sidebar test', async () => { waitUntil: 'networkidle', }); - const elements = await page.$$('h2 span'); - expect(elements.length).toBe(3); - - const configDir = elements[2]; - expect(await configDir.textContent()).toBe('config'); - await configDir.click(); - expect(page.url()).toBe( + const configLink = page + .locator('.rp-doc-layout__sidebar .rp-sidebar-item[data-depth="1"]') + .filter({ + has: page.locator('.rp-sidebar-item__left span', { + hasText: /^config$/, + }), + }); + await expect(configLink).toHaveCount(1); + await configLink.first().click(); + await expect(page).toHaveURL( `http://localhost:${appPort}/api/rspress-config/index.html`, ); }); diff --git a/e2e/fixtures/auto-nav-sidebar/index.test.ts b/e2e/fixtures/auto-nav-sidebar/index.test.ts index 421eb004b..7e6d0cda1 100644 --- a/e2e/fixtures/auto-nav-sidebar/index.test.ts +++ b/e2e/fixtures/auto-nav-sidebar/index.test.ts @@ -1,10 +1,10 @@ -import { type ElementHandle, expect, test } from '@playwright/test'; -import { getNavbar, getSidebar } from '../../utils/getSideBar'; +import { expect, test } from '@playwright/test'; +import { getNavbar } from '../../utils/getSideBar'; import { getPort, killProcess, runDevCommand } from '../../utils/runCommands'; test.describe('Auto nav and sidebar test', async () => { - let appPort; - let app; + let appPort: number; + let app: Awaited>; test.beforeAll(async () => { const appDir = __dirname; appPort = await getPort(); @@ -22,11 +22,21 @@ test.describe('Auto nav and sidebar test', async () => { waitUntil: 'networkidle', }); - const nav = await getNavbar(page); - expect(nav?.length).toBe(1); + const nav = getNavbar(page); + expect(await nav.count()).toBeGreaterThan(0); - const sidebar = await getSidebar(page); - expect(sidebar?.length).toBe(2); + const topLevelSidebarItems = page.locator( + '.rp-doc-layout__sidebar .rp-sidebar-item[data-depth="0"]', + ); + expect(await topLevelSidebarItems.count()).toBeGreaterThan(0); + + const topLevelTexts = await topLevelSidebarItems.allTextContents(); + const trimmedTopLevelTexts = topLevelTexts + .map(text => text.trim()) + .filter((text): text is string => text.length > 0); + expect(trimmedTopLevelTexts).toEqual( + expect.arrayContaining(['Guide', 'Advanced']), + ); }); test('Should load total API Overview correctly', async ({ page }) => { @@ -34,8 +44,10 @@ test.describe('Auto nav and sidebar test', async () => { waitUntil: 'networkidle', }); - const h2 = await page.$$('.overview-index h2'); - const h2Texts = await Promise.all(h2.map(element => element.textContent())); + const h2Elements = page.locator('.rp-overview h2.rspress-doc-outline span'); + const h2Texts = (await h2Elements.allTextContents()) + .map(text => text.trim()) + .filter(Boolean); expect(h2Texts.join(',')).toEqual( [ 'Config', @@ -47,9 +59,13 @@ test.describe('Auto nav and sidebar test', async () => { ].join(','), ); - const h3 = await page.$$('.overview-group_f8331 h3'); - const h3Texts = await Promise.all(h3.map(element => element.textContent())); - expect(h3Texts.join(',')).toEqual( + const itemTitles = page.locator( + '.rp-overview .rp-overview-group__item__title > a', + ); + const itemTitleTexts = (await itemTitles.allTextContents()) + .map(text => text.trim()) + .filter(Boolean); + expect(itemTitleTexts.join(',')).toEqual( [ 'Basic config', 'Theme config', @@ -66,9 +82,13 @@ test.describe('Auto nav and sidebar test', async () => { ].join(','), ); - const a = await page.$$('.overview-group_f8331 ul a'); - const aTexts = await Promise.all(a.map(element => element.textContent())); - expect(aTexts.join(',')).toEqual( + const links = page.locator( + '.rp-overview .rp-overview-group__item__content__item__link', + ); + const linkTexts = (await links.allTextContents()) + .map(text => text.trim()) + .filter(Boolean); + expect(linkTexts.join(',')).toEqual( [ 'root', 'logoText', @@ -93,8 +113,10 @@ test.describe('Auto nav and sidebar test', async () => { waitUntil: 'networkidle', }); - const h2 = await page.$$('.overview-index h2'); - const h2Texts = await Promise.all(h2.map(element => element.textContent())); + const h2Elements = page.locator('.rp-overview h2.rspress-doc-outline span'); + const h2Texts = (await h2Elements.allTextContents()) + .map(text => text.trim()) + .filter(Boolean); expect(h2Texts.join(',')).toEqual( [ 'Basic config', @@ -106,9 +128,13 @@ test.describe('Auto nav and sidebar test', async () => { ].join(','), ); - const h3 = await page.$$('.overview-group_f8331 h3'); - const h3Texts = await Promise.all(h3.map(element => element.textContent())); - expect(h3Texts.join(',')).toEqual( + const itemTitles = page.locator( + '.rp-overview .rp-overview-group__item__title > a', + ); + const itemTitleTexts = (await itemTitles.allTextContents()) + .map(text => text.trim()) + .filter(Boolean); + expect(itemTitleTexts.join(',')).toEqual( [ 'Basic config', 'Theme config', @@ -119,9 +145,13 @@ test.describe('Auto nav and sidebar test', async () => { ].join(','), ); - const a = await page.$$('.overview-group_f8331 ul a'); - const aTexts = await Promise.all(a.map(element => element.textContent())); - expect(aTexts.join(',')).toEqual( + const links = page.locator( + '.rp-overview .rp-overview-group__item__content__item__link', + ); + const linkTexts = (await links.allTextContents()) + .map(text => text.trim()) + .filter(Boolean); + expect(linkTexts.join(',')).toEqual( [ 'root', 'logoText', @@ -143,17 +173,29 @@ test.describe('Auto nav and sidebar test', async () => { waitUntil: 'networkidle', }); - const h2 = await page.$$('.overview-index h2'); - const h2Texts = await Promise.all(h2.map(element => element.textContent())); + const h2Elements = page.locator('.rp-overview h2.rspress-doc-outline span'); + const h2Texts = (await h2Elements.allTextContents()) + .map(text => text.trim()) + .filter(Boolean); expect(h2Texts.join(',')).toEqual(['Client API'].join(',')); - const h3 = await page.$$('.overview-group_f8331 h3'); - const h3Texts = await Promise.all(h3.map(element => element.textContent())); - expect(h3Texts.join(',')).toEqual(['Runtime API', 'Components'].join(',')); + const itemTitles = page.locator( + '.rp-overview .rp-overview-group__item__title > a', + ); + const itemTitleTexts = (await itemTitles.allTextContents()) + .map(text => text.trim()) + .filter(Boolean); + expect(itemTitleTexts.join(',')).toEqual( + ['Runtime API', 'Components'].join(','), + ); - const a = await page.$$('.overview-group_f8331 ul a'); - const aTexts = await Promise.all(a.map(element => element.textContent())); - expect(aTexts.join(',')).toEqual(['Usage', 'Example'].join(',')); + const links = page.locator( + '.rp-overview .rp-overview-group__item__content__item__link', + ); + const linkTexts = (await links.allTextContents()) + .map(text => text.trim()) + .filter(Boolean); + expect(linkTexts.join(',')).toEqual(['Usage', 'Example'].join(',')); }); test('Sidebar not have same name md/mdx will not navigate', async ({ @@ -162,7 +204,7 @@ test.describe('Auto nav and sidebar test', async () => { await page.goto(`http://localhost:${appPort}/guide/`, { waitUntil: 'networkidle', }); - await page.click('.rspress-scrollbar nav section div'); + await page.click('.rp-doc-layout__sidebar .rp-sidebar-group'); expect(page.url()).toBe(`http://localhost:${appPort}/guide/`); }); @@ -173,17 +215,27 @@ test.describe('Auto nav and sidebar test', async () => { waitUntil: 'networkidle', }); - const h2 = await page.$$('.overview-index h2'); - const h2Texts = await Promise.all(h2.map(element => element.textContent())); + const h2Elements = page.locator('.rp-overview h2.rspress-doc-outline span'); + const h2Texts = (await h2Elements.allTextContents()) + .map(text => text.trim()) + .filter(Boolean); expect(h2Texts.join(',')).toEqual(['Nested'].join(',')); - const h3 = await page.$$('.overview-group_f8331 h3'); - const h3Texts = await Promise.all(h3.map(element => element.textContent())); - expect(h3Texts.join(',')).toEqual(['Nested config'].join(',')); + const itemTitles = page.locator( + '.rp-overview .rp-overview-group__item__title > a', + ); + const itemTitleTexts = (await itemTitles.allTextContents()) + .map(text => text.trim()) + .filter(Boolean); + expect(itemTitleTexts.join(',')).toEqual(['Nested config'].join(',')); - const a = await page.$$('.overview-group_f8331 ul a'); - const aTexts = await Promise.all(a.map(element => element.textContent())); - expect(aTexts.join(',')).toEqual(['Nested H2'].join(',')); + const links = page.locator( + '.rp-overview .rp-overview-group__item__content__item__link', + ); + const linkTexts = (await links.allTextContents()) + .map(text => text.trim()) + .filter(Boolean); + expect(linkTexts.join(',')).toEqual(['Nested H2'].join(',')); }); test('Should generate data-context in sidebar group dom', async ({ @@ -193,52 +245,46 @@ test.describe('Auto nav and sidebar test', async () => { waitUntil: 'networkidle', }); - function getDataContextFromElements( - elements: ElementHandle[], - ) { - return page.evaluate( - sidebars => - sidebars?.map(sidebar => sidebar.getAttribute('data-context')), - elements, - ); - } - - const sidebarGroupSections = await page.$$('.rspress-sidebar-section'); - const c1 = await getDataContextFromElements(sidebarGroupSections); - expect(c1.join(',')).toEqual( - ['config', null, 'client-api', null].join(','), + const topLevelItems = page.locator( + '.rp-doc-layout__sidebar .rp-sidebar-item[data-depth="0"]', ); + const topLevelCount = await topLevelItems.count(); + const topLevelContexts = await Promise.all( + Array.from({ length: topLevelCount }, (_, i) => + topLevelItems.nth(i).getAttribute('data-context'), + ), + ); + expect(topLevelContexts.filter(Boolean)).toEqual([ + 'api-overview', + 'config', + 'client-api', + 'rspack-official-docsite-custom-link', + ]); - // Find sidebar elements with data-context="api-overview" - const overviewItems = await page.$$('[data-context="api-overview"]'); - - // Assert that there is at least one api-overview marker and the content is correct - expect(overviewItems.length).toEqual(1); + const overviewItems = page.locator('[data-context="api-overview"]'); + await expect(overviewItems).toHaveCount(1); - const sidebarGroupCollapses = await page.$$('.rspress-sidebar-collapse'); - const c2 = await page.evaluate( - sidebars => - sidebars?.map(sidebar => sidebar.getAttribute('data-context')), - sidebarGroupCollapses, + const depthOneItems = page.locator( + '.rp-doc-layout__sidebar .rp-sidebar-item[data-depth="1"]', ); - expect(c2.join(',')).toEqual( - ['config', null, 'client-api', null].join(','), + const depthOneCount = await depthOneItems.count(); + const depthOneContexts = await Promise.all( + Array.from({ length: depthOneCount }, (_, i) => + depthOneItems.nth(i).getAttribute('data-context'), + ), + ); + expect(depthOneContexts).toEqual( + expect.arrayContaining(['front-matter', 'config-build']), ); - const sidebarGroupItems = await page.$$('.rspress-sidebar-item'); - const c3 = await getDataContextFromElements(sidebarGroupItems); - // added the `depth=0 type=file` sidebar item with div.rspress-sidebar-item container - // so modify this to update test case - expect(c3?.[3]).toEqual('front-matter'); - expect(c3?.[4]).toEqual('config-build'); - - // custom link should work - const customLinkItems = await page.$$( - '[data-context="rspack-official-docsite-custom-link"]', + const customLinkItems = page.locator( + '.rp-doc-layout__sidebar [data-context="rspack-official-docsite-custom-link"]', ); - const c4 = await Promise.all(customLinkItems.map(i => i.textContent())); + const customLinkTexts = (await customLinkItems.allTextContents()) + .map(text => text?.trim()) + .filter(Boolean); - expect(c4.join(',')).toEqual( + expect(customLinkTexts.join(',')).toEqual( ['Rspack Official Docsite', 'Inner SideBar Rspack Official Docsite'].join( ',', ), diff --git a/e2e/fixtures/basic/index.test.ts b/e2e/fixtures/basic/index.test.ts index 431afd84d..94ef55f79 100644 --- a/e2e/fixtures/basic/index.test.ts +++ b/e2e/fixtures/basic/index.test.ts @@ -2,8 +2,8 @@ import { expect, test } from '@playwright/test'; import { getPort, killProcess, runDevCommand } from '../../utils/runCommands'; test.describe('basic test', async () => { - let appPort; - let app; + let appPort: number; + let app: Awaited>; test.beforeAll(async () => { const appDir = __dirname; appPort = await getPort(); @@ -18,62 +18,46 @@ test.describe('basic test', async () => { test('Index page', async ({ page }) => { await page.goto(`http://localhost:${appPort}`); - const h1 = await page.$('h1'); - const text = await page.evaluate(h1 => h1?.textContent, h1); - expect(text).toContain('Hello world'); - // expect the .header-anchor to be rendered and take the correct href - const headerAnchor = await page.$('.header-anchor'); - const href = await page.evaluate( - headerAnchor => headerAnchor?.getAttribute('href'), - headerAnchor, - ); - expect(href).toBe('#hello-world'); + await expect(page.locator('h1')).toContainText('Hello world'); + const headerAnchor = page.locator('.rp-header-anchor').first(); + await expect(headerAnchor).toHaveAttribute('href', '#hello-world'); }); test('404 page', async ({ page }) => { await page.goto(`http://localhost:${appPort}/404`, { waitUntil: 'networkidle', }); - // find the 404 text in the page - const text = await page.evaluate(() => document.body.textContent); - expect(text).toContain('404'); + await expect(page.locator('body')).toContainText('404'); }); test('dark mode', async ({ page }) => { await page.goto(`http://localhost:${appPort}`, { waitUntil: 'networkidle', }); - const darkModeButton = await page.$('.rspress-nav-appearance'); - const html = await page.$('html'); - let htmlClass = await page.evaluate( - html => html?.getAttribute('class'), - html, - ); - const defaultMode = htmlClass?.includes('dark') ? 'dark' : 'light'; - await darkModeButton?.click(); - // check the class in html - htmlClass = await page.evaluate(html => html?.getAttribute('class'), html); - expect(Boolean(htmlClass?.includes('dark'))).toBe(defaultMode !== 'dark'); - // click the button again, check the class in html - await darkModeButton?.click(); - htmlClass = await page.evaluate(html => html?.getAttribute('class'), html); - expect(Boolean(htmlClass?.includes('dark'))).toBe(defaultMode === 'dark'); + const appearanceToggle = page.locator('.rp-switch-appearance').first(); + const getIsDark = () => + page.evaluate(() => + document.documentElement.classList.contains('rp-dark'), + ); + const defaultIsDark = await getIsDark(); + await appearanceToggle.click(); + await expect.poll(getIsDark).toBe(!defaultIsDark); + await appearanceToggle.click(); + await expect.poll(getIsDark).toBe(defaultIsDark); }); test('Hover over social links', async ({ page }) => { await page.goto(`http://localhost:${appPort}`); - await page.hover('.social-links'); - await page.waitForTimeout(1000); - const logoLink = await page.$('a[href="/zh"]'); - expect(logoLink).not.toBeNull(); + const socialLinks = page.locator('.rp-social-links').first(); + await socialLinks.hover(); + await expect( + page.locator('.rp-social-links__item a[href="/zh"]').first(), + ).toBeVisible(); }); test('globalStyles should work', async ({ page }) => { await page.goto(`http://localhost:${appPort}`); - const link = await page.$('.rspress-doc a'); - const colorValue = await link?.evaluate( - element => getComputedStyle(element).color, - ); - expect(colorValue).toEqual('rgb(255, 165, 0)'); + const link = page.locator('.rp-doc a').first(); + await expect(link).toHaveCSS('color', 'rgb(255, 165, 0)'); }); }); diff --git a/e2e/fixtures/basic/rspress.config.ts b/e2e/fixtures/basic/rspress.config.ts index 4e8d65eec..f485d308a 100644 --- a/e2e/fixtures/basic/rspress.config.ts +++ b/e2e/fixtures/basic/rspress.config.ts @@ -10,7 +10,7 @@ export default defineConfig({ icon: 'github', mode: 'dom', content: - '', + '', }, ], }, diff --git a/e2e/fixtures/check-dead-link/index.test.ts b/e2e/fixtures/check-dead-link/index.test.ts index 70fe0fc7b..ca39afcfa 100644 --- a/e2e/fixtures/check-dead-link/index.test.ts +++ b/e2e/fixtures/check-dead-link/index.test.ts @@ -7,8 +7,8 @@ import { } from '../../utils/runCommands'; test.describe('check dead links', async () => { - let appPort; - let app; + let appPort: number; + let app: Awaited>; test.afterAll(async () => { if (app) { await killProcess(app); @@ -21,19 +21,26 @@ test.describe('check dead links', async () => { await runBuildCommand(appDir); app = await runPreviewCommand(appDir, appPort); + const getLinks = async (url: string) => { + await page.goto(url, { + waitUntil: 'networkidle', + }); + return page + .locator('.rp-doc a') + .evaluateAll(elements => + elements + .map(element => element.getAttribute('href')) + .filter( + (href): href is string => + typeof href === 'string' && !href.startsWith('#'), + ), + ); + }; + { - await page.goto( + const links = await getLinks( `http://localhost:${appPort}/base/guide/basic/quick-start`, - { - waitUntil: 'networkidle', - }, ); - // Get all the element - const linkDoms = await page.$$('.rspress-doc a'); - - const links = ( - await Promise.all(linkDoms.map(linkDom => linkDom.getAttribute('href'))) - ).filter(i => !i?.startsWith('#')); expect(links).toEqual([ '/base/guide/basic/install.html', '/base/guide/basic/install.html', @@ -55,18 +62,9 @@ test.describe('check dead links', async () => { ]); } { - await page.goto( + const links = await getLinks( `http://localhost:${appPort}/base/en/guide/basic/quick-start`, - { - waitUntil: 'networkidle', - }, ); - // Get all the element - const linkDoms = await page.$$('.rspress-doc a'); - - const links = ( - await Promise.all(linkDoms.map(linkDom => linkDom.getAttribute('href'))) - ).filter(i => !i?.startsWith('#')); expect(links).toEqual([ '/base/en/guide/basic/install.html', '/base/en/guide/basic/install.html', @@ -95,19 +93,26 @@ test.describe('check dead links', async () => { await runBuildCommand(appDir, 'rspress-no-prefix.config.ts'); app = await runPreviewCommand(appDir, appPort); + const getLinks = async (url: string) => { + await page.goto(url, { + waitUntil: 'networkidle', + }); + return page + .locator('.rp-doc a') + .evaluateAll(elements => + elements + .map(element => element.getAttribute('href')) + .filter( + (href): href is string => + typeof href === 'string' && !href.startsWith('#'), + ), + ); + }; + { - await page.goto( + const links = await getLinks( `http://localhost:${appPort}/base/guide/basic/quick-start`, - { - waitUntil: 'networkidle', - }, ); - // Get all the element - const linkDoms = await page.$$('.rspress-doc a'); - - const links = ( - await Promise.all(linkDoms.map(linkDom => linkDom.getAttribute('href'))) - ).filter(i => !i?.startsWith('#')); expect(links).toEqual([ '/base/guide/basic/install.html', '/base/guide/basic/install.html', @@ -129,18 +134,9 @@ test.describe('check dead links', async () => { ]); } { - await page.goto( + const links = await getLinks( `http://localhost:${appPort}/base/en/guide/basic/quick-start`, - { - waitUntil: 'networkidle', - }, ); - // Get all the element - const linkDoms = await page.$$('.rspress-doc a'); - - const links = ( - await Promise.all(linkDoms.map(linkDom => linkDom.getAttribute('href'))) - ).filter(i => !i?.startsWith('#')); expect(links).toEqual([ '/base/guide/basic/install.html', '/base/guide/basic/install.html', @@ -169,19 +165,26 @@ test.describe('check dead links', async () => { await runBuildCommand(appDir, 'rspress-clean.config.ts'); app = await runPreviewCommand(appDir, appPort); + const getLinks = async (url: string) => { + await page.goto(url, { + waitUntil: 'networkidle', + }); + return page + .locator('.rp-doc a') + .evaluateAll(elements => + elements + .map(element => element.getAttribute('href')) + .filter( + (href): href is string => + typeof href === 'string' && !href.startsWith('#'), + ), + ); + }; + { - await page.goto( + const links = await getLinks( `http://localhost:${appPort}/base/guide/basic/quick-start`, - { - waitUntil: 'networkidle', - }, ); - // Get all the element - const linkDoms = await page.$$('.rspress-doc a'); - - const links = ( - await Promise.all(linkDoms.map(linkDom => linkDom.getAttribute('href'))) - ).filter(i => !i?.startsWith('#')); expect(links).toEqual([ '/base/guide/basic/install', '/base/guide/basic/install', @@ -204,18 +207,9 @@ test.describe('check dead links', async () => { } { - await page.goto( + const links = await getLinks( `http://localhost:${appPort}/base/en/guide/basic/quick-start`, - { - waitUntil: 'networkidle', - }, ); - // Get all the element - const linkDoms = await page.$$('.rspress-doc a'); - - const links = ( - await Promise.all(linkDoms.map(linkDom => linkDom.getAttribute('href'))) - ).filter(i => !i?.startsWith('#')); expect(links).toEqual([ '/base/en/guide/basic/install', '/base/en/guide/basic/install', diff --git a/e2e/fixtures/client-redirects/index.test.ts b/e2e/fixtures/client-redirects/index.test.ts index 445d48902..411c6fcd1 100644 --- a/e2e/fixtures/client-redirects/index.test.ts +++ b/e2e/fixtures/client-redirects/index.test.ts @@ -2,8 +2,8 @@ import { expect, test } from '@playwright/test'; import { getPort, killProcess, runDevCommand } from '../../utils/runCommands'; test.describe('client redirects test', async () => { - let appPort; - let app; + let appPort: number; + let app: Awaited>; test.beforeAll(async () => { const appDir = __dirname; appPort = await getPort(); @@ -20,43 +20,43 @@ test.describe('client redirects test', async () => { await page.goto(`http://localhost:${appPort}/docs/old1`, { waitUntil: 'networkidle', }); - expect(page.url()).toBe(`http://localhost:${appPort}/docs/new1`); + await expect(page).toHaveURL(`http://localhost:${appPort}/docs/new1`); }); test('Should redirect correctly - array', async ({ page }) => { await page.goto(`http://localhost:${appPort}/docs/2022`, { waitUntil: 'networkidle', }); - expect(page.url()).toBe(`http://localhost:${appPort}/docs/2024`); + await expect(page).toHaveURL(`http://localhost:${appPort}/docs/2024`); await page.goto(`http://localhost:${appPort}/docs/2023/new`, { waitUntil: 'networkidle', }); - expect(page.url()).toBe(`http://localhost:${appPort}/docs/2024/new`); + await expect(page).toHaveURL(`http://localhost:${appPort}/docs/2024/new`); }); test('Should redirect correctly - reg1', async ({ page }) => { await page.goto(`http://localhost:${appPort}/docs/old2`, { waitUntil: 'networkidle', }); - expect(page.url()).toBe(`http://localhost:${appPort}/docs/new2`); + await expect(page).toHaveURL(`http://localhost:${appPort}/docs/new2`); await page.goto(`http://localhost:${appPort}/docs/old2/foo`, { waitUntil: 'networkidle', }); - expect(page.url()).toBe(`http://localhost:${appPort}/docs/new2/foo`); + await expect(page).toHaveURL(`http://localhost:${appPort}/docs/new2/foo`); }); test('Should redirect correctly - reg2', async ({ page }) => { await page.goto(`http://localhost:${appPort}/docs/old3`, { waitUntil: 'networkidle', }); - expect(page.url()).toBe(`http://localhost:${appPort}/docs/new3`); + await expect(page).toHaveURL(`http://localhost:${appPort}/docs/new3`); await page.goto(`http://localhost:${appPort}/foo/docs/old3`, { waitUntil: 'networkidle', }); - expect(page.url()).toBe(`http://localhost:${appPort}/foo/docs/new3`); + await expect(page).toHaveURL(`http://localhost:${appPort}/foo/docs/new3`); }); test('Should redirect correctly - external', async ({ page }) => { @@ -74,6 +74,6 @@ test.describe('client redirects test', async () => { waitUntil: 'networkidle', }); - expect(page.url()).toBe(externalUrl); + await expect(page).toHaveURL(externalUrl); }); }); diff --git a/e2e/fixtures/code-block-runtime/index.test.ts b/e2e/fixtures/code-block-runtime/index.test.ts index 3c5aa4e69..851335d7d 100644 --- a/e2e/fixtures/code-block-runtime/index.test.ts +++ b/e2e/fixtures/code-block-runtime/index.test.ts @@ -2,8 +2,8 @@ import { expect, test } from '@playwright/test'; import { getPort, killProcess, runDevCommand } from '../../utils/runCommands'; test.describe('', async () => { - let appPort; - let app; + let appPort: number; + let app: Awaited>; test.beforeAll(async () => { const appDir = __dirname; appPort = await getPort(); @@ -22,22 +22,19 @@ test.describe('', async () => { await page.goto(`http://localhost:${appPort}`, { waitUntil: 'networkidle', }); - const containers = await page.$$('div.language-js'); - expect(containers.length).toBe(2); + const containers = page.locator('div.language-js'); + await expect(containers).toHaveCount(2); - // CodeBlockRuntime should render the same result with compile-time code block - for (const container of containers) { - const title = await container.$('.rspress-code-title'); - expect(await title!.textContent()).toBe('test.js'); - const content = await container.$('.rspress-code-content'); - expect( - await content!.evaluate(el => - el.classList.contains('rspress-scrollbar'), - ), - ).toBe(true); - const shikiContainer = await content?.$('.shiki.css-variables'); - expect(await shikiContainer?.evaluate(el => el.tagName)).toBe('PRE'); - expect(await shikiContainer?.$eval('code', el => el.textContent)).toBe( + const containerCount = await containers.count(); + for (let index = 0; index < containerCount; index += 1) { + const container = containers.nth(index); + await expect(container.locator('.rspress-code-title')).toHaveText( + 'test.js', + ); + const content = container.locator('.rspress-code-content'); + const shikiContainer = content.locator('.shiki.css-variables').first(); + await expect(shikiContainer).toHaveJSProperty('tagName', 'PRE'); + await expect(shikiContainer.locator('code').first()).toHaveText( "console.log('Hello CodeBlock!');", ); } @@ -47,22 +44,19 @@ test.describe('', async () => { await page.goto(`http://localhost:${appPort}/highlight`, { waitUntil: 'networkidle', }); - const containers = await page.$$('div.language-ts'); - expect(containers.length).toBe(2); + const containers = page.locator('div.language-ts'); + await expect(containers).toHaveCount(2); - // CodeBlockRuntime should render the same result with compile-time code block - for (const container of containers) { - const title = await container.$('.rspress-code-title'); - expect(await title!.textContent()).toBe('highlight.ts'); - const content = await container.$('.rspress-code-content'); - expect( - await content!.evaluate(el => - el.classList.contains('rspress-scrollbar'), - ), - ).toBe(true); - const shikiContainer = await content?.$('.shiki.css-variables'); - expect(await shikiContainer?.evaluate(el => el.tagName)).toBe('PRE'); - expect(await shikiContainer?.$eval('code', el => el.textContent)).toBe( + const containerCount = await containers.count(); + for (let index = 0; index < containerCount; index += 1) { + const container = containers.nth(index); + await expect(container.locator('.rspress-code-title')).toHaveText( + 'highlight.ts', + ); + const content = container.locator('.rspress-code-content'); + const shikiContainer = content.locator('.shiki.css-variables').first(); + await expect(shikiContainer).toHaveJSProperty('tagName', 'PRE'); + await expect(shikiContainer.locator('code').first()).toHaveText( "console.log('Highlighted'); \nconsole.log('Highlighted');\nconsole.log('Not highlighted');", ); } diff --git a/e2e/fixtures/custom-headers/index.test.ts b/e2e/fixtures/custom-headers/index.test.ts index 625a8a9cf..c30bc4786 100644 --- a/e2e/fixtures/custom-headers/index.test.ts +++ b/e2e/fixtures/custom-headers/index.test.ts @@ -7,8 +7,8 @@ import { } from '../../utils/runCommands'; test.describe('custom headers', async () => { - let appPort; - let app; + let appPort: number; + let app: Awaited>; test.beforeAll(async () => { const appDir = __dirname; appPort = await getPort(); @@ -26,8 +26,7 @@ test.describe('custom headers', async () => { await page.goto(`http://localhost:${appPort}`, { waitUntil: 'networkidle', }); - const titleDoms = await page.$$('title'); - expect(titleDoms.length).toBe(1); + await expect(page.locator('title')).toHaveCount(1); }); test('config headers should be injected', async ({ page }) => { @@ -35,32 +34,26 @@ test.describe('custom headers', async () => { waitUntil: 'networkidle', }); - const configStringMetaContent = await page.$eval( - 'meta[name="config-string-head"]', - configStringMeta => configStringMeta.getAttribute('content'), - ); - expect(configStringMetaContent).toEqual('config-string-head-value'); + await expect( + page.locator('meta[name="config-string-head"]'), + ).toHaveAttribute('content', 'config-string-head-value'); - const configTupleMetaContent = await page.getAttribute( - 'meta[name="config-tuple-head"]', - 'content', - ); - expect(configTupleMetaContent).toEqual('config-tuple-head-value'); + await expect( + page.locator('meta[name="config-tuple-head"]'), + ).toHaveAttribute('content', 'config-tuple-head-value'); - const configFnStringMetaContent = await page.$eval( - 'meta[name="config-fn-string-head"]', - configFnStringMeta => configFnStringMeta.getAttribute('content'), - ); + const configFnStringMetaContent = await page + .locator('meta[name="config-fn-string-head"]') + .getAttribute('content'); expect( configFnStringMetaContent?.endsWith( 'e2e/fixtures/custom-headers/doc/index.mdx', ), ).toBeTruthy(); - const configFnTupleMetaContent = await page.getAttribute( - 'meta[name="config-fn-tuple-head"]', - 'content', - ); + const configFnTupleMetaContent = await page + .locator('meta[name="config-fn-tuple-head"]') + .getAttribute('content'); expect( configFnTupleMetaContent?.endsWith( 'e2e/fixtures/custom-headers/doc/index.mdx', @@ -73,23 +66,20 @@ test.describe('custom headers', async () => { waitUntil: 'networkidle', }); - const customMetaContent = await page.$eval( - 'meta[name="custom-meta"]', - customMeta => customMeta.getAttribute('content'), + await expect(page.locator('meta[name="custom-meta"]')).toHaveAttribute( + 'content', + 'custom-meta-content', ); - expect(customMetaContent).toEqual('custom-meta-content'); - const customMetaContent2 = await page.getAttribute( - 'meta[name="custom-meta-2"]', + await expect(page.locator('meta[name="custom-meta-2"]')).toHaveAttribute( 'content', + 'custom-meta-content-2', ); - expect(customMetaContent2).toEqual('custom-meta-content-2'); - const customHttpEquiv = await page.$eval( - 'meta[http-equiv="refresh"]', - customMeta => customMeta.getAttribute('content'), + await expect(page.locator('meta[http-equiv="refresh"]')).toHaveAttribute( + 'content', + '300', ); - expect(customHttpEquiv).toEqual('300'); const htmlContent = await page.content(); expect(htmlContent).toContain( diff --git a/e2e/fixtures/custom-home-footer/index.test.ts b/e2e/fixtures/custom-home-footer/index.test.ts index 975623176..3cb0c8476 100644 --- a/e2e/fixtures/custom-home-footer/index.test.ts +++ b/e2e/fixtures/custom-home-footer/index.test.ts @@ -2,8 +2,8 @@ import { expect, test } from '@playwright/test'; import { getPort, killProcess, runDevCommand } from '../../utils/runCommands'; test.describe('home footer test', async () => { - let appPort; - let app; + let appPort: number; + let app: Awaited>; test.beforeAll(async () => { const appDir = __dirname; appPort = await getPort(); @@ -18,13 +18,8 @@ test.describe('home footer test', async () => { test('custom home footer', async ({ page }) => { await page.goto(`http://localhost:${appPort}`); - await page.waitForSelector('footer'); - const footer = await page.$('footer'); - expect(footer).not.toBeNull(); - if (!footer) { - return; - } - const a = await footer.$('a'); - expect(a).not.toBeNull(); + const footer = page.locator('footer'); + await expect(footer).toBeVisible(); + await expect(footer.locator('a')).toBeVisible(); }); }); diff --git a/e2e/fixtures/custom-icon/index.test.ts b/e2e/fixtures/custom-icon/index.test.ts index 3a81c309d..bb32ecf5f 100644 --- a/e2e/fixtures/custom-icon/index.test.ts +++ b/e2e/fixtures/custom-icon/index.test.ts @@ -2,8 +2,8 @@ import { expect, test } from '@playwright/test'; import { getPort, killProcess, runDevCommand } from '../../utils/runCommands'; test.describe('custom icon test', async () => { - let appPort; - let app; + let appPort: number; + let app: Awaited>; test.beforeAll(async () => { const appDir = __dirname; appPort = await getPort(); @@ -20,15 +20,12 @@ test.describe('custom icon test', async () => { await page.goto(`http://localhost:${appPort}`, { waitUntil: 'networkidle', }); - const h1 = await page.$('h1'); - const text = await page.evaluate(h1 => h1?.textContent, h1); - await expect(text).toContain('Hello world'); + await expect(page.locator('h1')).toContainText('Hello world'); - const headerAnchor = await page.$('.rspress-nav-search-button img'); - const src = await page.evaluate( - headerAnchor => headerAnchor?.getAttribute('src'), - headerAnchor, + const searchIcon = page.locator('.rp-search-button__content img').first(); + await expect(searchIcon).toHaveAttribute( + 'src', + /data:image\/svg\+xml;base64/, ); - expect(src).toContain('data:image/svg+xml;base64'); }); }); diff --git a/e2e/fixtures/custom-layout-ui-switch/doc/index.mdx b/e2e/fixtures/custom-layout-ui-switch/doc/index.mdx deleted file mode 100644 index 716ed1421..000000000 --- a/e2e/fixtures/custom-layout-ui-switch/doc/index.mdx +++ /dev/null @@ -1 +0,0 @@ -# Hello world diff --git a/e2e/fixtures/custom-layout-ui-switch/index.test.ts b/e2e/fixtures/custom-layout-ui-switch/index.test.ts deleted file mode 100644 index 34920fb61..000000000 --- a/e2e/fixtures/custom-layout-ui-switch/index.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { getPort, killProcess, runDevCommand } from '../../utils/runCommands'; - -test.describe('custom layout ui switch', async () => { - let appPort; - let app; - test.beforeAll(async () => { - const appDir = __dirname; - appPort = await getPort(); - app = await runDevCommand(appDir, appPort); - }); - - test.afterAll(async () => { - if (app) { - await killProcess(app); - } - }); - - test('Index page', async ({ page }) => { - await page.goto(`http://localhost:${appPort}`, { - waitUntil: 'networkidle', - }); - const elementExists = (await page.$('.rspress-nav')) !== null; - expect(elementExists).toEqual(false); - }); -}); diff --git a/e2e/fixtures/custom-layout-ui-switch/package.json b/e2e/fixtures/custom-layout-ui-switch/package.json deleted file mode 100644 index c3b016c26..000000000 --- a/e2e/fixtures/custom-layout-ui-switch/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "@rspress-fixture/custom-layout-ui-switch", - "version": "1.0.0", - "private": true, - "scripts": { - "build": "rspress build", - "dev": "rspress dev", - "preview": "rspress preview" - }, - "dependencies": { - "@rspress/core": "workspace:*" - }, - "devDependencies": { - "@types/node": "^22.8.1" - } -} diff --git a/e2e/fixtures/custom-layout-ui-switch/rspress.config.ts b/e2e/fixtures/custom-layout-ui-switch/rspress.config.ts deleted file mode 100644 index f3aefd04c..000000000 --- a/e2e/fixtures/custom-layout-ui-switch/rspress.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import * as path from 'node:path'; -import { defineConfig } from '@rspress/core'; - -export default defineConfig({ - root: path.join(__dirname, 'doc'), -}); diff --git a/e2e/fixtures/custom-layout-ui-switch/theme/index.tsx b/e2e/fixtures/custom-layout-ui-switch/theme/index.tsx deleted file mode 100644 index 91b375769..000000000 --- a/e2e/fixtures/custom-layout-ui-switch/theme/index.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { Layout as BaseLayout } from '@rspress/core/theme'; - -const Layout = () => { - return ( - - ); -}; - -export { Layout }; -export * from '@rspress/core/theme'; diff --git a/e2e/fixtures/custom-layout-ui-switch/tsconfig.json b/e2e/fixtures/custom-layout-ui-switch/tsconfig.json deleted file mode 100644 index 936218cee..000000000 --- a/e2e/fixtures/custom-layout-ui-switch/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "lib": ["DOM", "ES2020"], - "module": "ESNext", - "jsx": "react-jsx", - "noEmit": true, - "strict": true, - "skipLibCheck": true, - "isolatedModules": true, - "resolveJsonModule": true, - "moduleResolution": "bundler", - "useDefineForClassFields": true, - "allowImportingTsExtensions": true - }, - "include": ["docs", "theme", "rspress.config.ts"], - "mdx": { - "checkMdx": true - } -} diff --git a/e2e/fixtures/custom-plugin/index.test.ts b/e2e/fixtures/custom-plugin/index.test.ts index e41c62969..1d3cd3c98 100644 --- a/e2e/fixtures/custom-plugin/index.test.ts +++ b/e2e/fixtures/custom-plugin/index.test.ts @@ -2,8 +2,8 @@ import { expect, test } from '@playwright/test'; import { getPort, killProcess, runDevCommand } from '../../utils/runCommands'; test.describe('plugin test', async () => { - let appPort; - let app; + let appPort: number; + let app: Awaited>; test.beforeAll(async () => { const appDir = __dirname; appPort = await getPort(); @@ -20,10 +20,8 @@ test.describe('plugin test', async () => { await page.goto(`http://localhost:${appPort}/filepath-route`, { waitUntil: 'networkidle', }); - const h1 = await page.getByRole('heading', { - name: /Demo1/, - }); - await expect(h1).toBeTruthy(); + const heading = page.getByRole('heading', { name: /Demo1/ }); + await expect(heading).toBeVisible(); }); test('Should add route by content', async ({ page }) => { @@ -31,9 +29,7 @@ test.describe('plugin test', async () => { waitUntil: 'networkidle', }); - const h1 = await page.getByRole('heading', { - name: /Demo2/, - }); - await expect(h1).toBeTruthy(); + const heading = page.getByRole('heading', { name: /Demo2/ }); + await expect(heading).toBeVisible(); }); }); diff --git a/e2e/fixtures/dynamic-toc/index.test.ts b/e2e/fixtures/dynamic-toc/index.test.ts index 9537fc44d..31dad8c70 100644 --- a/e2e/fixtures/dynamic-toc/index.test.ts +++ b/e2e/fixtures/dynamic-toc/index.test.ts @@ -1,12 +1,10 @@ -import { setTimeout } from 'node:timers/promises'; - import { expect, test } from '@playwright/test'; import { getPort, killProcess, runDevCommand } from '../../utils/runCommands'; test.describe('dynamic toc', async () => { - let appPort; - let app; + let appPort: number; + let app: Awaited>; test.beforeAll(async () => { const appDir = __dirname; appPort = await getPort(); @@ -24,22 +22,16 @@ test.describe('dynamic toc', async () => { waitUntil: 'networkidle', }); - let h2 = await page.$('h2'); - let text = await page.evaluate(h2 => h2?.textContent!.trim(), h2); - expect(text).toBe('#Term'); - - let toc = await page.$('.aside-link'); - let tocText = await page.evaluate(toc => toc?.textContent!.trim(), toc); - expect(tocText).toBe('Term'); + const heading = page.locator('h2'); + await expect(heading).toContainText('Term'); - await setTimeout(1000); // Wait for dynamic TOC to update + const tocItem = page + .locator('.rp-toc-item .rp-aside__toc-item__text') + .first(); + await expect(tocItem).toHaveText('Term'); - h2 = await page.$('h2'); - text = await page.evaluate(h2 => h2?.textContent!.trim(), h2); - expect(text).toBe('#Term dynamic & content'); + await expect(heading).toContainText('Term dynamic & content'); - toc = await page.$('.aside-link'); - tocText = await page.evaluate(toc => toc?.textContent!.trim(), toc); - expect(tocText).toBe('Term dynamic & content'); + await expect(tocItem).toHaveText('Term dynamic & content'); }); }); diff --git a/e2e/fixtures/github-alert-mdxjs/index.test.ts b/e2e/fixtures/github-alert-mdxjs/index.test.ts index 212384160..d691ea029 100644 --- a/e2e/fixtures/github-alert-mdxjs/index.test.ts +++ b/e2e/fixtures/github-alert-mdxjs/index.test.ts @@ -21,30 +21,31 @@ test.describe('github alert syntax in mdx-js', async () => { }) => { await page.goto(`http://localhost:${appPort}`); - const topLevelDirectives = await page.$$( - '.rspress-doc > [class^="rspress-directive"]', - ); - - expect(topLevelDirectives.length).toEqual(7); + const topLevelCallouts = page.locator('.rspress-doc > .rp-callout'); + await expect(topLevelCallouts).toHaveCount(7); - const listDirectives = await page.$$( - '.rspress-doc > * > li > [class^="rspress-directive"]', + const listCallouts = page.locator( + '.rspress-doc > ol li > .rp-callout, .rspress-doc > ul li > .rp-callout', ); - expect(listDirectives.length).toEqual(2); + await expect(listCallouts).toHaveCount(2); + + const stepsCallouts = page.locator('.rp-steps .rp-callout'); + await expect(stepsCallouts).toHaveCount(2); + + const allCallouts = [topLevelCallouts, listCallouts, stepsCallouts]; + const containerTypes: string[] = []; + + for (const calloutsLocator of allCallouts) { + const count = await calloutsLocator.count(); + for (let i = 0; i < count; i++) { + const className = await calloutsLocator.nth(i).getAttribute('class'); + const modifier = className + ?.split(' ') + .find(name => name.startsWith('rp-callout--')); + containerTypes.push(modifier?.replace('rp-callout--', '') || ''); + } + } - const stepsDirectives = await page.$$( - '.rspress-doc > .\\[counter-reset\\:step\\] * > li > [class^="rspress-directive"]', - ); - expect(stepsDirectives.length).toEqual(2); - - const containerTypes = await Promise.all( - [...topLevelDirectives, ...listDirectives, ...stepsDirectives].map( - async directive => { - const className = await directive.getAttribute('class'); - return className?.split(' ')[1]; - }, - ), - ); expect(containerTypes).toEqual([ 'tip', 'note', diff --git a/e2e/fixtures/github-alert-mdxjs/rspress.config.ts b/e2e/fixtures/github-alert-mdxjs/rspress.config.ts index f3aefd04c..4165836c9 100644 --- a/e2e/fixtures/github-alert-mdxjs/rspress.config.ts +++ b/e2e/fixtures/github-alert-mdxjs/rspress.config.ts @@ -3,4 +3,11 @@ import { defineConfig } from '@rspress/core'; export default defineConfig({ root: path.join(__dirname, 'doc'), + markdown: { + link: { + checkDeadLinks: { + excludes: ['bar'], + }, + }, + }, }); diff --git a/e2e/fixtures/heading-title/index.test.ts b/e2e/fixtures/heading-title/index.test.ts index 402d1b44f..c9cff9ca6 100644 --- a/e2e/fixtures/heading-title/index.test.ts +++ b/e2e/fixtures/heading-title/index.test.ts @@ -2,8 +2,8 @@ import { expect, test } from '@playwright/test'; import { getPort, killProcess, runDevCommand } from '../../utils/runCommands'; test.describe('heading-title test', async () => { - let appPort; - let app; + let appPort: number; + let app: Awaited>; test.beforeAll(async () => { const appDir = __dirname; appPort = await getPort(); @@ -29,7 +29,7 @@ test.describe('heading-title test', async () => { expect(fontSize).toBe('32px'); // check anchor #heading-title should be in h1 - const anchor = h1.locator('a.header-anchor'); + const anchor = h1.locator('.rp-header-anchor'); await expect(anchor).toHaveAttribute('href', '#heading-title'); }); }); diff --git a/e2e/fixtures/hide-nav-bar/doc/index.mdx b/e2e/fixtures/hide-nav-bar/doc/index.mdx deleted file mode 100644 index e7e195e70..000000000 --- a/e2e/fixtures/hide-nav-bar/doc/index.mdx +++ /dev/null @@ -1,99 +0,0 @@ ---- -pageType: custom ---- - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT - -A LOT OF CONTENT diff --git a/e2e/fixtures/hide-nav-bar/index.test.ts b/e2e/fixtures/hide-nav-bar/index.test.ts deleted file mode 100644 index f1efb218c..000000000 --- a/e2e/fixtures/hide-nav-bar/index.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { expect, type Page, test } from '@playwright/test'; -import { getPort, killProcess, runDevCommand } from '../../utils/runCommands'; - -async function isNavBarVisible(page: Page): Promise { - const nav = await page.$('.rspress-nav'); - const className: string = await nav?.evaluate(el => el.className); - return !className.includes('hidden'); -} - -async function scrollDown(page: Page) { - // Simulate the scrolling of people - await page.mouse.wheel(0, 100); - await page.waitForTimeout(100); - await page.mouse.wheel(0, 100); - await page.waitForTimeout(100); -} - -test.describe('basic test', async () => { - let appPort; - let app; - async function launchApp(configFile: string) { - const appDir = __dirname; - appPort = await getPort(); - app = await runDevCommand(appDir, appPort, configFile); - } - - test.afterAll(async () => { - if (app) { - await killProcess(app); - } - }); - - test('hideNavBar: "auto" should work', async ({ page }) => { - await launchApp('./rspress-hide-auto.config.ts'); - await page.goto(`http://localhost:${appPort}/`); - await scrollDown(page); - expect(await isNavBarVisible(page)).toBeFalsy(); - }); - - test('Navbar should be visible on mobile when we scroll down with hideNavbar to never', async ({ - page, - }) => { - await launchApp('./rspress.config.ts'); - await page.setViewportSize({ width: 375, height: 812 }); - - await page.goto(`http://localhost:${appPort}/`); - - await scrollDown(page); - expect(await isNavBarVisible(page)).toBeTruthy(); - }); -}); diff --git a/e2e/fixtures/hide-nav-bar/package.json b/e2e/fixtures/hide-nav-bar/package.json deleted file mode 100644 index 0cb614531..000000000 --- a/e2e/fixtures/hide-nav-bar/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "@rspress-fixture/rspress-hide-nav-bar", - "version": "1.0.0", - "private": true, - "scripts": { - "build": "rspress build", - "dev": "rspress dev", - "preview": "rspress preview" - }, - "dependencies": { - "@rspress/core": "workspace:*" - }, - "devDependencies": { - "@types/node": "^22.8.1" - } -} diff --git a/e2e/fixtures/hide-nav-bar/rspress-hide-auto.config.ts b/e2e/fixtures/hide-nav-bar/rspress-hide-auto.config.ts deleted file mode 100644 index 8da2386bb..000000000 --- a/e2e/fixtures/hide-nav-bar/rspress-hide-auto.config.ts +++ /dev/null @@ -1,13 +0,0 @@ -import * as path from 'node:path'; -import { defineConfig } from '@rspress/core'; - -export default defineConfig({ - root: path.join(__dirname, 'doc'), - route: { - cleanUrls: true, - }, - themeConfig: { - hideNavbar: 'auto', - nav: [], - }, -}); diff --git a/e2e/fixtures/hide-nav-bar/rspress.config.ts b/e2e/fixtures/hide-nav-bar/rspress.config.ts deleted file mode 100644 index 2e172ff7f..000000000 --- a/e2e/fixtures/hide-nav-bar/rspress.config.ts +++ /dev/null @@ -1,13 +0,0 @@ -import * as path from 'node:path'; -import { defineConfig } from '@rspress/core'; - -export default defineConfig({ - root: path.join(__dirname, 'doc'), - route: { - cleanUrls: true, - }, - themeConfig: { - hideNavbar: 'never', - nav: [], - }, -}); diff --git a/e2e/fixtures/hide-nav-bar/tsconfig.json b/e2e/fixtures/hide-nav-bar/tsconfig.json deleted file mode 100644 index 936218cee..000000000 --- a/e2e/fixtures/hide-nav-bar/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "lib": ["DOM", "ES2020"], - "module": "ESNext", - "jsx": "react-jsx", - "noEmit": true, - "strict": true, - "skipLibCheck": true, - "isolatedModules": true, - "resolveJsonModule": true, - "moduleResolution": "bundler", - "useDefineForClassFields": true, - "allowImportingTsExtensions": true - }, - "include": ["docs", "theme", "rspress.config.ts"], - "mdx": { - "checkMdx": true - } -} diff --git a/e2e/fixtures/hmr/index.test.ts b/e2e/fixtures/hmr/index.test.ts index e715cf97d..6dcd6d751 100644 --- a/e2e/fixtures/hmr/index.test.ts +++ b/e2e/fixtures/hmr/index.test.ts @@ -13,8 +13,8 @@ const TEST_NAV_FILE = path.resolve(__dirname, 'doc/_nav.json'); const TEST_META_FILE = path.resolve(__dirname, 'doc/guide/_meta.json'); test.describe('hmr test', async () => { - let appPort; - let app; + let appPort: number; + let app: Awaited>; let originalContent: string; let originalFragmentContent: string; let originalNavContent: string; @@ -42,44 +42,44 @@ test.describe('hmr test', async () => { test('Test page', async ({ page }) => { await page.goto(`http://localhost:${appPort}/guide/test.html`); - await page.waitForSelector('p:text("Hello world")'); // basic - await expect(page.locator('p:text("Hello world")')).toBeVisible(); + const helloParagraph = page.locator('p', { hasText: 'Hello world' }); + await expect(helloParagraph).toBeVisible(); await fs.writeFile( TEST_FILE, originalContent.replace('Hello world', 'Hello hmr world'), ); - await expect(page.locator('p:text("Hello hmr world")')).toBeVisible(); + await expect( + page.locator('p', { hasText: 'Hello hmr world' }), + ).toBeVisible(); // file code block - await expect(page.locator('text=This is mdx fragment')).toBeVisible(); + await expect(page.getByText('This is mdx fragment')).toBeVisible(); await fs.writeFile( TEST_FRAGMENT_FILE, originalFragmentContent.replace('This is', 'This is hmr'), ); - await expect(page.locator('text=This is hmr mdx fragment')).toBeVisible(); + await expect(page.getByText('This is hmr mdx fragment')).toBeVisible(); // _nav.json await expect( - page.locator('.rspress-nav-menu .rspress-nav-menu-item:text("Guide")'), + page.locator('.rp-nav-menu__item', { hasText: 'Guide' }), ).toBeVisible(); await fs.writeFile( TEST_NAV_FILE, originalNavContent.replace('"Guide"', '"HMR Guide"'), ); await expect( - page.locator( - '.rspress-nav-menu .rspress-nav-menu-item:text("HMR Guide")', - ), + page.locator('.rp-nav-menu__item', { hasText: 'HMR Guide' }), ).toBeVisible(); // _meta.json await expect( - page.locator('.rspress-sidebar-item :text("Test")'), + page.locator('.rp-sidebar-item span', { hasText: 'Test' }), ).toBeVisible(); await fs.writeFile(TEST_META_FILE, '["foo"]'); await expect( - page.locator('.rspress-sidebar-item :text("Foo")'), + page.locator('.rp-sidebar-item span', { hasText: 'Foo' }), ).toBeVisible(); }); }); diff --git a/e2e/fixtures/i18n/index.test.ts b/e2e/fixtures/i18n/index.test.ts index 97d905f19..6b2786f6d 100644 --- a/e2e/fixtures/i18n/index.test.ts +++ b/e2e/fixtures/i18n/index.test.ts @@ -2,8 +2,8 @@ import { expect, test } from '@playwright/test'; import { getPort, killProcess, runDevCommand } from '../../utils/runCommands'; test.describe('i18n test', async () => { - let appPort; - let app; + let appPort: number; + let app: Awaited>; test.beforeAll(async () => { const appDir = __dirname; appPort = await getPort(); @@ -22,23 +22,23 @@ test.describe('i18n test', async () => { waitUntil: 'networkidle', }); - const h1 = await page.$('h1'); - const text = await page.evaluate(h1 => h1?.textContent, h1); - await expect(text).toContain('首页'); + const heading = page.locator('h1').first(); + await expect(heading).toContainText('首页'); - const button = await page.$('.translation .rspress-nav-menu-group-button')!; - expect(button).toBeTruthy(); - // hover the button - await button!.hover(); - // click the button content to switch to English - const buttonContent = await page.$( - '.translation .rspress-nav-menu-group-content > div > div:nth-child(2)', + const languageSwitcher = page.locator( + '.rp-nav__others .rp-nav-menu__item__container', + { + hasText: '简体中文', + }, ); - const buttonContentText = await page.evaluate( - buttonContent => buttonContent?.textContent, - buttonContent, - ); - expect(buttonContentText).toBe('English'); + await expect(languageSwitcher).toBeVisible(); + await languageSwitcher.hover(); + + const englishOption = page.locator('.rp-hover-group__item a', { + hasText: 'English', + }); + await expect(englishOption).toBeVisible(); + await expect(englishOption).toHaveText('English'); }); test('Add language prefix in route automatically when current language is not default language', async ({ @@ -48,21 +48,21 @@ test.describe('i18n test', async () => { waitUntil: 'networkidle', }); // take the `click` button - let link = await page.getByRole('link', { + const absoluteLink = page.getByRole('link', { name: /absolute/, }); - expect(link).toBeTruthy(); - // check the compile result of absolute link in doc content - expect(await link.getAttribute('href')).toBe( + await expect(absoluteLink).toBeVisible(); + await expect(absoluteLink).toHaveAttribute( + 'href', '/en/guide/basic/install.html', ); - link = await page.getByRole('link', { + + const relativeLink = page.getByRole('link', { name: /relative/, }); - - // check the compile result of relative link in doc content - expect(link).toBeTruthy(); - expect(await link.getAttribute('href')).toBe( + await expect(relativeLink).toBeVisible(); + await expect(relativeLink).toHaveAttribute( + 'href', '/en/guide/basic/install.html', ); }); @@ -74,37 +74,42 @@ test.describe('i18n test', async () => { waitUntil: 'networkidle', }); // check the compile result of absolute link in doc content - let link = await page.getByRole('link', { + const absoluteLink = page.getByRole('link', { name: /绝对路径/, }); - expect(link).toBeTruthy(); - expect(await link.getAttribute('href')).toBe('/guide/basic/install.html'); + await expect(absoluteLink).toBeVisible(); + await expect(absoluteLink).toHaveAttribute( + 'href', + '/guide/basic/install.html', + ); // check the compile result of relative link in doc content - link = await page.getByRole('link', { + const relativeLink = page.getByRole('link', { name: /相对路径/, }); - expect(link).toBeTruthy(); - expect(await link.getAttribute('href')).toBe('/guide/basic/install.html'); + await expect(relativeLink).toBeVisible(); + await expect(relativeLink).toHaveAttribute( + 'href', + '/guide/basic/install.html', + ); }); test('Should render sidebar correctly', async ({ page }) => { await page.goto(`http://localhost:${appPort}/guide/basic/quick-start`, { waitUntil: 'networkidle', }); - // take the sidebar - const sidebar = await page.$$( - '.rspress-sidebar .rspress-scrollbar > nav > section', + const sidebarGroups = page.locator( + '.rp-doc-layout__sidebar .rp-sidebar-group', ); - expect(sidebar?.length).toBe(1); + await expect(sidebarGroups).toHaveCount(1); }); - test('Should not render appearance switch button', async ({ page }) => { + test('Should render appearance switch button', async ({ page }) => { await page.goto(`http://localhost:${appPort}/guide/basic/quick-start`, { waitUntil: 'networkidle', }); - // take the appearance switch button - const button = await page.$('.rspress-nav-appearance'); - expect(button).toBeFalsy(); + const button = page.locator('.rp-switch-appearance'); + await expect(button).toHaveCount(1); + await expect(button).toBeVisible(); }); test('Should not 404 after redirecting in first visit', async ({ page }) => { @@ -128,52 +133,60 @@ test.describe('i18n test', async () => { await page.goto(`http://localhost:${appPort}/guide/basic/quick-start`, { waitUntil: 'networkidle', }); - const dirContent = await page.textContent( - '.rspress-sidebar .rspress-scrollbar nav section', - ); - expect(dirContent).toContain('基本'); - - const sectionHeaderContent = await page.textContent( - '.rspress-sidebar-section-header span', - ); - expect(sectionHeaderContent).toEqual('成长'); + const dirContent = await page + .locator('.rp-doc-layout__sidebar') + .textContent(); + expect(dirContent ?? '').toContain('基本'); + + const sectionHeaderContent = await page + .locator('.rp-sidebar-section-header__left span') + .textContent(); + expect(sectionHeaderContent ?? '').toEqual('成长'); }); test('Should render i18n sidebar - en', async ({ page }) => { await page.goto(`http://localhost:${appPort}/en/guide/basic/quick-start`, { waitUntil: 'networkidle', }); - const dirContent = await page.textContent( - '.rspress-sidebar .rspress-scrollbar nav section', - ); - expect(dirContent).toContain('Basic'); - - const sectionHeaderContent = await page.textContent( - '.rspress-sidebar-section-header span', - ); - expect(sectionHeaderContent).toEqual('Growth'); + const dirContent = await page + .locator('.rp-doc-layout__sidebar') + .textContent(); + expect(dirContent ?? '').toContain('Basic'); + + const sectionHeaderContent = await page + .locator('.rp-sidebar-section-header__left span') + .textContent(); + expect(sectionHeaderContent ?? '').toEqual('Growth'); }); test('Should add routePrefix when type is custom-link', async ({ page }) => { await page.goto(`http://localhost:${appPort}/guide/basic/quick-start`, { waitUntil: 'networkidle', }); - const customLinkZh = await page.$('nav > div.rspress-sidebar-item > a'); - const hrefZh = await page.evaluate( - customLinkZh => customLinkZh?.getAttribute('href'), - customLinkZh, + const customLinkZh = page.locator( + '.rp-doc-layout__sidebar a.rp-sidebar-item', + { + hasText: 'My Link', + }, + ); + await expect(customLinkZh).toHaveAttribute( + 'href', + '/guide/basic/install.html', ); - expect(hrefZh).toBe('/guide/basic/install.html'); await page.goto(`http://localhost:${appPort}/en/guide/basic/quick-start`, { waitUntil: 'networkidle', }); - const customLinkEn = await page.$('nav > div.rspress-sidebar-item > a'); - const hrefEn = await page.evaluate( - customLinkEn => customLinkEn?.getAttribute('href'), - customLinkEn, + const customLinkEn = page.locator( + '.rp-doc-layout__sidebar a.rp-sidebar-item', + { + hasText: 'My Link', + }, + ); + await expect(customLinkEn).toHaveAttribute( + 'href', + '/en/guide/basic/install.html', ); - expect(hrefEn).toBe('/en/guide/basic/install.html'); }); test('Should not crash when switch language in api page', async ({ @@ -182,19 +195,30 @@ test.describe('i18n test', async () => { await page.goto(`http://localhost:${appPort}/api`, { waitUntil: 'networkidle', }); - const overviewContentZh = await page.textContent('.overview-index'); - expect(overviewContentZh).toContain('Overview'); - expect(overviewContentZh).toContain('zh'); - expect(overviewContentZh).not.toContain('en'); - await page.click('.rspress-nav-menu-group-button'); - await page.waitForTimeout(100); - await page.click('.rspress-nav-menu-group-content a'); - await page.waitForTimeout(100); - const content = await page.textContent('#root'); - expect(content).not.toEqual(''); - const overviewContentEn = await page.textContent('.overview-index'); - expect(overviewContentEn).toContain('Overview'); - expect(overviewContentEn).toContain('en'); - expect(overviewContentEn).not.toContain('zh'); + const overviewContentZh = await page.locator('.rp-overview').textContent(); + expect(overviewContentZh ?? '').toContain('Overview'); + expect(overviewContentZh ?? '').toContain('zh'); + expect(overviewContentZh ?? '').not.toContain('en'); + + const languageSwitcher = page + .locator('.rp-nav-menu__item') + .filter({ hasText: '简体中文' }); + await languageSwitcher.hover(); + const englishOption = languageSwitcher.locator('.rp-hover-group__item a', { + hasText: 'English', + }); + await englishOption.click(); + + const newLanguageSwitcher = page + .locator('.rp-nav-menu__item') + .filter({ hasText: 'English' }); + await expect(newLanguageSwitcher).toBeVisible(); + + const content = await page.textContent('#__rspress_root'); + expect(content ?? '').not.toEqual(''); + const overviewContentEn = await page.locator('.rp-overview').textContent(); + expect(overviewContentEn ?? '').toContain('Overview'); + expect(overviewContentEn ?? '').toContain('en'); + expect(overviewContentEn ?? '').not.toContain('zh'); }); }); diff --git a/e2e/fixtures/inline-markdown/index.test.ts b/e2e/fixtures/inline-markdown/index.test.ts index 8ea2d3acd..ea16532ce 100644 --- a/e2e/fixtures/inline-markdown/index.test.ts +++ b/e2e/fixtures/inline-markdown/index.test.ts @@ -1,10 +1,9 @@ import { expect, test } from '@playwright/test'; -import { getSidebar } from '../../utils/getSideBar'; import { getPort, killProcess, runDevCommand } from '../../utils/runCommands'; test.describe('Inline markdown test', async () => { - let appPort; - let app; + let appPort: number; + let app: Awaited>; test.beforeAll(async () => { const appDir = __dirname; appPort = await getPort(); @@ -24,11 +23,11 @@ test.describe('Inline markdown test', async () => { waitUntil: 'networkidle', }); - const sidebar = await getSidebar(page); - expect(sidebar?.length).toBe(9); + const sidebar = page.locator('.rp-doc-layout__sidebar .rp-sidebar-item'); + await expect(sidebar).toHaveCount(9); - const sidebarTexts = await Promise.all( - sidebar.map(element => element.textContent()), + const sidebarTexts = (await sidebar.allTextContents()).map(text => + text.trim(), ); expect(sidebarTexts.join(',')).toEqual( [ @@ -44,19 +43,22 @@ test.describe('Inline markdown test', async () => { ].join(','), ); + const sidebarCount = await sidebar.count(); const sidebarInnerHtml = await Promise.all( - sidebar.map(element => element.innerHTML()), + Array.from({ length: sidebarCount }, (_, index) => + sidebar.nth(index).locator('.rp-sidebar-item__left span').innerHTML(), + ), ); const expectedSidebarInnerHtml = [ - 'Overview', - 'Class: Component<P, S>', - 'Class: Component<P, S>', - 'bold', - 'emphasis', - '<foo>', - '-m <number>', - 'delete', - 'link', + 'Overview', + 'Class: Component<P, S>', + 'Class: Component<P, S>', + 'bold', + 'emphasis', + '<foo>', + '-m <number>', + 'delete', + 'link', ]; for (const [index, html] of sidebarInnerHtml.entries()) { expect(html).toContain(expectedSidebarInnerHtml[index]); @@ -70,9 +72,11 @@ test.describe('Inline markdown test', async () => { waitUntil: 'networkidle', }); - const h2 = await page.$$('.overview-index h2'); - const h2Texts = await Promise.all(h2.map(element => element.textContent())); - expect(h2Texts.join(',')).toEqual( + const overviewHeadingLocator = page.locator( + '.rp-overview h2.rspress-doc-outline span', + ); + const overviewHeadings = await overviewHeadingLocator.allTextContents(); + expect(overviewHeadings.map(text => text.trim()).join(',')).toEqual( [ 'Class: Component', 'Class: Component', @@ -84,10 +88,14 @@ test.describe('Inline markdown test', async () => { 'link', ].join(','), ); - const h2InnerHtml = await Promise.all( - h2.map(element => element.innerHTML()), + + const overviewHeadingCount = await overviewHeadingLocator.count(); + const overviewHeadingHtml = await Promise.all( + Array.from({ length: overviewHeadingCount }, (_, index) => + overviewHeadingLocator.nth(index).innerHTML(), + ), ); - expect(h2InnerHtml.join(',')).toEqual( + expect(overviewHeadingHtml.join(',')).toEqual( [ 'Class: Component<P, S>', 'Class: Component<P, S>', @@ -100,58 +108,66 @@ test.describe('Inline markdown test', async () => { ].join(','), ); - const h3 = await page.$$('.overview-group_f8331 h3'); - const h3Texts = await Promise.all(h3.map(element => element.textContent())); - expect(h3Texts.join(',')).toEqual( - [ - 'Class: Component', - 'Class: Component', - 'bold', - 'emphasis', - '', - '-m ', - 'delete', - 'link', - ].join(','), + const overviewTitles = page.locator( + '.rp-overview .rp-overview-group__item__title > a', ); - const h3InnerHtml = await Promise.all( - h3.map(element => element.innerHTML()), + await expect(overviewTitles).toHaveText([ + 'Class: Component', + 'Class: Component', + 'bold', + 'emphasis', + '', + '-m ', + 'delete', + 'link', + ]); + + const overviewTitlesCount = await overviewTitles.count(); + const titleInnerHtml = await Promise.all( + Array.from({ length: overviewTitlesCount }, (_, index) => + overviewTitles.nth(index).innerHTML(), + ), ); - const expectedH3InnerHtml = [ - 'Class: Component<P, S>', - 'Class: Component<P, S>', - 'bold', - 'emphasis', - '<foo>', - '-m <number>', - 'delete', + const expectedTitleInnerHtml = [ + 'Class: Component<P, S>', + 'Class: Component<P, S>', + 'bold', + 'emphasis', + '<foo>', + '-m <number>', + 'delete', 'link', ]; - for (const [index, html] of h3InnerHtml.entries()) { - expect(html).toContain(expectedH3InnerHtml[index]); + for (const [index, html] of titleInnerHtml.entries()) { + expect(html).toContain(expectedTitleInnerHtml[index]); } - const a = await page.$$('.overview-group_f8331 ul a'); - const aTexts = await Promise.all(a.map(element => element.textContent())); - expect(aTexts.join(',')).toEqual( - [ - 'Class: Component', - 'Class: Component', - '-m ', - '', - 'foo baz', - 'bold', - 'emphasis', - 'delete', - 'This is a long string to test regex performance', - 'this is link rsbuild', - 'this is bold link', // FIXME: should be 'this is bold link rsbuild' - 'this is code link', - 'this is bold code link', - ].join(','), + const overviewLinks = page.locator( + '.rp-overview .rp-overview-group__item__content__item__link', + ); + await expect(overviewLinks).toHaveText([ + 'Class: Component', + 'Class: Component', + '-m ', + '', + 'foo baz', + 'bold', + 'emphasis', + 'delete', + 'This is a long string to test regex performance', + 'this is link rsbuild', + 'this is bold link', // FIXME: should be 'this is bold link rsbuild' + 'this is code link', + 'this is bold code link', + ]); + + const overviewLinkCount = await overviewLinks.count(); + const overviewLinkInnerHtml = await Promise.all( + Array.from({ length: overviewLinkCount }, (_, index) => + overviewLinks.nth(index).innerHTML(), + ), ); - const aInnerHtml = await Promise.all(a.map(element => element.innerHTML())); - const expectedAInnerHtml = [ + const expectedOverviewLinkInnerHtml = [ 'Class: Component<P, S>', 'Class: Component<P, S>', '-m <number>', @@ -161,13 +177,13 @@ test.describe('Inline markdown test', async () => { 'emphasis', 'delete', 'This is a long string to test regex performance', - `this is link rsbuild`, - `this is bold link`, // FIXME: should be 'this is bold link rsbuild' - `this is code link`, - `this is bold code link`, + 'this is link rsbuild', + 'this is bold link', + 'this is code link', + 'this is bold code link', ]; - for (const [index, html] of aInnerHtml.entries()) { - expect(html).toContain(expectedAInnerHtml[index]); + for (const [index, html] of overviewLinkInnerHtml.entries()) { + expect(html).toContain(expectedOverviewLinkInnerHtml[index]); } }); @@ -176,11 +192,13 @@ test.describe('Inline markdown test', async () => { waitUntil: 'networkidle', }); - const asides = await page.$$('.aside-link-text'); - const asidesTexts = await Promise.all( - asides.map(element => element.textContent()), + const asideItems = page.locator('.rp-aside__toc-item__text'); + await expect(asideItems).toHaveCount(9); + + const asideTexts = (await asideItems.allTextContents()).map(text => + text.trim(), ); - expect(asidesTexts.join(',')).toEqual( + expect(asideTexts.join(',')).toEqual( [ 'Class: Component', 'Class: Component', @@ -193,10 +211,14 @@ test.describe('Inline markdown test', async () => { 'This is a long string to test regex performance', ].join(','), ); - const asidesInnerHtml = await Promise.all( - asides.map(element => element.innerHTML()), + + const asideCount = await asideItems.count(); + const asideInnerHtml = await Promise.all( + Array.from({ length: asideCount }, (_, index) => + asideItems.nth(index).innerHTML(), + ), ); - expect(asidesInnerHtml.join(',')).toEqual( + expect(asideInnerHtml.join(',')).toEqual( [ 'Class: Component<P, S>', 'Class: Component<P, S>', @@ -219,10 +241,10 @@ test.describe('Inline markdown test', async () => { }); // Check h1 element - const h1 = await page.$('h1#class-componentp-s'); - expect(h1).not.toBeNull(); - const h1Anchor = await h1?.$('a.header-anchor'); - expect(await h1Anchor?.getAttribute('href')).toBe('#class-componentp-s'); + const h1 = page.locator('h1#class-componentp-s'); + await expect(h1).toHaveCount(1); + const h1Anchor = h1.locator('a.rp-header-anchor'); + await expect(h1Anchor).toHaveAttribute('href', '#class-componentp-s'); // Check h2 elements const h2Selectors = [ @@ -237,10 +259,11 @@ test.describe('Inline markdown test', async () => { ]; for (const selector of h2Selectors) { - const h2 = await page.$(selector); - expect(h2).not.toBeNull(); - const h2Anchor = await h2?.$('a.header-anchor'); - expect(await h2Anchor?.getAttribute('href')).toBe( + const h2 = page.locator(selector); + await expect(h2).toHaveCount(1); + const h2Anchor = h2.locator('a.rp-header-anchor'); + await expect(h2Anchor).toHaveAttribute( + 'href', `#${selector.split('#')[1]}`, ); } @@ -253,9 +276,10 @@ test.describe('Inline markdown test', async () => { waitUntil: 'networkidle', }); - const img = await page.$('img'); - expect(img).not.toBeNull(); - expect(await img?.getAttribute('src')).toBe( + const img = page.locator('img').first(); + await expect(img).toBeVisible(); + await expect(img).toHaveAttribute( + 'src', 'https://assets.rspack.rs/rspress/rspress-logo-480x480.png', ); }); diff --git a/e2e/fixtures/issue-1309/index.test.ts b/e2e/fixtures/issue-1309/index.test.ts index 45764144b..40c93d209 100644 --- a/e2e/fixtures/issue-1309/index.test.ts +++ b/e2e/fixtures/issue-1309/index.test.ts @@ -1,10 +1,10 @@ import { expect, test } from '@playwright/test'; -import { getNavbar, getSidebar } from '../../utils/getSideBar'; +import { getNavbarItems, getSidebar } from '../../utils/getSideBar'; import { getPort, killProcess, runDevCommand } from '../../utils/runCommands'; test.describe('issue-1309', async () => { - let appPort; - let app; + let appPort: number; + let app: Awaited>; test.beforeAll(async () => { const appDir = __dirname; appPort = await getPort(); @@ -19,21 +19,21 @@ test.describe('issue-1309', async () => { test('should not generate the sidebar in homePage', async ({ page }) => { await page.goto(`http://localhost:${appPort}`); - const nav = await getNavbar(page); - expect(nav?.length).toBe(1); + const navItems = getNavbarItems(page); + await expect(navItems).toHaveCount(1); - const sidebar = await getSidebar(page); - expect(sidebar?.length).toBe(0); + const sidebar = getSidebar(page); + expect(await sidebar.count()).toBe(0); }); test('should render the sidebar correctly in guide page', async ({ page, }) => { await page.goto(`http://localhost:${appPort}/guide`); - const nav = await getNavbar(page); - expect(nav?.length).toBe(1); + const navItems = getNavbarItems(page); + await expect(navItems).toHaveCount(1); - const sidebar = await getSidebar(page); - expect(sidebar?.length).toBe(3); + const sidebar = getSidebar(page); + expect(await sidebar.count()).toBe(4); }); }); diff --git a/e2e/fixtures/markdown-link/index.test.ts b/e2e/fixtures/markdown-link/index.test.ts index efb81c4e0..5fb49d765 100644 --- a/e2e/fixtures/markdown-link/index.test.ts +++ b/e2e/fixtures/markdown-link/index.test.ts @@ -18,12 +18,12 @@ test.describe('basic test', async () => { test('all links should be normalized', async ({ page }) => { await page.goto(`http://localhost:${appPort}/base/guide`); - const links = await page.$$('.rspress-doc ul li a'); + const links = page.locator('.rspress-doc ul li a'); + const count = await links.count(); const urls = await Promise.all( - links.map(async link => { - const href = await link.getAttribute('href'); - return href; - }), + Array.from({ length: count }, (_, i) => + links.nth(i).getAttribute('href'), + ), ); expect(urls).toEqual([ diff --git a/e2e/fixtures/multi-version/index.test.ts b/e2e/fixtures/multi-version/index.test.ts index d5dfb209c..a6fe5b353 100644 --- a/e2e/fixtures/multi-version/index.test.ts +++ b/e2e/fixtures/multi-version/index.test.ts @@ -2,8 +2,8 @@ import { expect, test } from '@playwright/test'; import { getPort, killProcess, runDevCommand } from '../../utils/runCommands'; test.describe('Multi version test', async () => { - let appPort; - let app; + let appPort: number; + let app: Awaited>; test.beforeAll(async () => { const appDir = __dirname; appPort = await getPort(); @@ -18,29 +18,25 @@ test.describe('Multi version test', async () => { test('Default version and default language', async ({ page }) => { await page.goto(`http://localhost:${appPort}`); - const h1 = await page.$('h1'); - const text = await page.evaluate(h1 => h1?.textContent, h1); - await expect(text).toContain('v1'); + const h1 = page.locator('h1'); + await expect(h1).toContainText('v1'); }); test('Not Default version default language', async ({ page }) => { await page.goto(`http://localhost:${appPort}/v2`); - const h1 = await page.$('h1'); - const text = await page.evaluate(h1 => h1?.textContent, h1); - await expect(text).toContain('v2'); + const h1 = page.locator('h1'); + await expect(h1).toContainText('v2'); }); test('Default version not default language', async ({ page }) => { await page.goto(`http://localhost:${appPort}/zh`); - const h1 = await page.$('h1'); - const text = await page.evaluate(h1 => h1?.textContent, h1); - await expect(text).toContain('v1 中文'); + const h1 = page.locator('h1'); + await expect(h1).toContainText('v1 中文'); }); test('Not default version not default language', async ({ page }) => { await page.goto(`http://localhost:${appPort}/v2/zh`); - const h1 = await page.$('h1'); - const text = await page.evaluate(h1 => h1?.textContent, h1); - await expect(text).toContain('v2 中文'); + const h1 = page.locator('h1'); + await expect(h1).toContainText('v2 中文'); }); }); diff --git a/e2e/fixtures/nav-link-item-with-hash/index.test.ts b/e2e/fixtures/nav-link-item-with-hash/index.test.ts index dfd23e3ab..af5c03020 100644 --- a/e2e/fixtures/nav-link-item-with-hash/index.test.ts +++ b/e2e/fixtures/nav-link-item-with-hash/index.test.ts @@ -2,8 +2,8 @@ import { expect, test } from '@playwright/test'; import { getPort, killProcess, runDevCommand } from '../../utils/runCommands'; test.describe('basic test', async () => { - let appPort; - let app; + let appPort: number; + let app: Awaited>; test.beforeAll(async () => { const appDir = __dirname; appPort = await getPort(); @@ -19,10 +19,10 @@ test.describe('basic test', async () => { test('Navigate with an hash as link', async ({ page }) => { await page.goto(`http://localhost:${appPort}/`); - await page.locator('.rspress-nav-menu a').first().click(); + await page.locator('.rp-nav-menu__item a').first().click(); expect(page.url()).toContain('/#pageA'); - await page.locator('.rspress-nav-menu a').nth(1).click(); + await page.locator('.rp-nav-menu__item a').nth(1).click(); expect(page.url()).toContain('/#pageB'); }); @@ -33,10 +33,11 @@ test.describe('basic test', async () => { await page.goto(`http://localhost:${appPort}/`); - await page.locator('.rspress-mobile-hamburger').click(); - await expect(page.locator('.rspress-nav-screen')).toBeVisible(); + await page.locator('.rp-nav-hamburger').first().click(); + const navScreen = page.locator('.rp-nav-screen'); + await expect(navScreen).toHaveClass(/rp-nav-screen--open/); await page.getByRole('link', { name: 'PageC' }).click(); - await expect(page.locator('.rspress-nav-screen')).not.toBeVisible(); + await expect(navScreen).not.toHaveClass(/rp-nav-screen--open/); }); }); diff --git a/e2e/fixtures/nav-link-item-with-hash/rspress.config.ts b/e2e/fixtures/nav-link-item-with-hash/rspress.config.ts index 0ff00de06..470e6123a 100644 --- a/e2e/fixtures/nav-link-item-with-hash/rspress.config.ts +++ b/e2e/fixtures/nav-link-item-with-hash/rspress.config.ts @@ -7,7 +7,6 @@ export default defineConfig({ cleanUrls: true, }, themeConfig: { - hideNavbar: 'never', nav: [ { text: 'PageA', diff --git a/e2e/fixtures/nav-link-items-without-suffix/doc/_nav.json b/e2e/fixtures/nav-link-items-without-suffix/doc/_nav.json deleted file mode 100644 index 974234fe2..000000000 --- a/e2e/fixtures/nav-link-items-without-suffix/doc/_nav.json +++ /dev/null @@ -1,18 +0,0 @@ -[ - { - "text": "OnlyLink", - "link": "/only-link/" - }, - { - "text": "OnlyItems", - "items": [{ "text": "Item", "link": "/only-items/item" }] - }, - { - "text": "ItemsAndLink", - "link": "/items-and-link/", - "items": [ - { "text": "Child1", "link": "/items-and-link/child-1" }, - { "text": "Child2", "link": "/items-and-link/child-2" } - ] - } -] diff --git a/e2e/fixtures/nav-link-items-without-suffix/doc/index.md b/e2e/fixtures/nav-link-items-without-suffix/doc/index.md deleted file mode 100644 index 716ed1421..000000000 --- a/e2e/fixtures/nav-link-items-without-suffix/doc/index.md +++ /dev/null @@ -1 +0,0 @@ -# Hello world diff --git a/e2e/fixtures/nav-link-items-without-suffix/doc/items-and-link/child-1.md b/e2e/fixtures/nav-link-items-without-suffix/doc/items-and-link/child-1.md deleted file mode 100644 index 0bb8680d9..000000000 --- a/e2e/fixtures/nav-link-items-without-suffix/doc/items-and-link/child-1.md +++ /dev/null @@ -1 +0,0 @@ -# child-1 diff --git a/e2e/fixtures/nav-link-items-without-suffix/doc/items-and-link/child-2.md b/e2e/fixtures/nav-link-items-without-suffix/doc/items-and-link/child-2.md deleted file mode 100644 index 89c16a303..000000000 --- a/e2e/fixtures/nav-link-items-without-suffix/doc/items-and-link/child-2.md +++ /dev/null @@ -1 +0,0 @@ -# child-2 diff --git a/e2e/fixtures/nav-link-items-without-suffix/doc/only-items/item.md b/e2e/fixtures/nav-link-items-without-suffix/doc/only-items/item.md deleted file mode 100644 index ef3116688..000000000 --- a/e2e/fixtures/nav-link-items-without-suffix/doc/only-items/item.md +++ /dev/null @@ -1 +0,0 @@ -# only-items diff --git a/e2e/fixtures/nav-link-items-without-suffix/doc/only-link/index.md b/e2e/fixtures/nav-link-items-without-suffix/doc/only-link/index.md deleted file mode 100644 index 30fd2b347..000000000 --- a/e2e/fixtures/nav-link-items-without-suffix/doc/only-link/index.md +++ /dev/null @@ -1 +0,0 @@ -# only-link diff --git a/e2e/fixtures/nav-link-items-without-suffix/index.test.ts b/e2e/fixtures/nav-link-items-without-suffix/index.test.ts deleted file mode 100644 index 3dfa03e92..000000000 --- a/e2e/fixtures/nav-link-items-without-suffix/index.test.ts +++ /dev/null @@ -1,116 +0,0 @@ -import type { Locator, Page } from '@playwright/test'; -import { expect, test } from '@playwright/test'; -import { getPort, killProcess, runDevCommand } from '../../utils/runCommands'; - -test.describe('Nav should functions well', async () => { - let appPort: number; - let app: unknown; - let _navMenu: Locator; - let navMenuItems: Locator[]; - let onlyItemsButton: Locator; - let _onlyItemsContainer: Locator; - let onlyItemsChildren: Locator[]; - let itemsAndLinkButton: Locator; - let itemsAndLinkChildren: Locator[]; - let _itemsAndLinkContainer: Locator; - - const init = async (page: Page) => { - await page.goto(`http://localhost:${appPort}`, { - waitUntil: 'networkidle', - }); - - // ElementHandler is currently discouraged by official - // use Locator instead - // Please refer to https://playwright.dev/docs/api/class-elementhandle - await page.waitForSelector('.rspress-nav-menu'); - - _navMenu = page.locator('.rspress-nav-menu'); - navMenuItems = await page.locator('.rspress-nav-menu > *').all(); - - onlyItemsButton = navMenuItems[1].locator('.rspress-nav-menu-group-button'); - onlyItemsChildren = await navMenuItems[1] - .locator('.rspress-nav-menu-group-content a') - .all(); - _onlyItemsContainer = navMenuItems[1].locator( - '.rspress-nav-menu-group-content', - ); - - itemsAndLinkButton = navMenuItems[2].locator( - '.rspress-nav-menu-group-button', - ); - itemsAndLinkChildren = await navMenuItems[2] - .locator('.rspress-nav-menu-group-content a') - .all(); - _itemsAndLinkContainer = navMenuItems[2].locator( - '.rspress-nav-menu-group-content', - ); - }; - - const gotoPage = (suffix: string) => `http://localhost:${appPort}${suffix}`; - - test.beforeAll(async () => { - const appDir = __dirname; - appPort = await getPort(); - app = await runDevCommand(appDir, appPort); - }); - - test.afterAll(async () => { - if (app) { - await killProcess(app); - } - }); - - test('sidebar should redirect and render correctly', async ({ page }) => { - await page.goto(`http://localhost:${appPort}/items-and-link/child-1`, { - waitUntil: 'networkidle', - }); - const navItems = await page.locator('.rspress-sidebar nav a').all(); - await navItems[1].click(); - const content = await page.innerText('.rspress-doc'); - expect(content).toContain('child-2'); - }); - - test('bottom link should redirect and render correctly', async ({ page }) => { - await page.goto(`http://localhost:${appPort}/items-and-link/child-1`, { - waitUntil: 'networkidle', - }); - const navItems = await page.locator('footer a').all(); - await navItems[0].click(); - await page.waitForURL('**/child-2'); - await page.waitForSelector('.rspress-doc'); - const content = await page.innerText('.rspress-doc'); - expect(content).toContain('child-2'); - }); - - test('it should be able to redirect correctly', async ({ page }) => { - await init(page); - - await navMenuItems[0].click(); - await page.waitForURL('**/only-link/'); - expect(page.url()).toBe(gotoPage('/only-link/')); - - await onlyItemsButton.hover(); - await onlyItemsChildren[0].click(); - await page.waitForURL('**/item'); - expect(page.url()).toBe(gotoPage('/only-items/item')); - - await itemsAndLinkButton.click(); - expect(page.url()).toBe(gotoPage('/items-and-link/')); - - await page.goto(gotoPage('/'), { - waitUntil: 'networkidle', - }); - await itemsAndLinkButton.hover(); - await itemsAndLinkChildren[0].click({ force: true, timeout: 1000 }); - await page.waitForURL('**/child-1'); - expect(page.url()).toBe(gotoPage('/items-and-link/child-1')); - - await page.goto(gotoPage('/'), { - waitUntil: 'networkidle', - }); - await itemsAndLinkButton.hover(); - await itemsAndLinkChildren[1].click({ force: true, timeout: 1000 }); - await page.waitForURL('**/child-2'); - expect(page.url()).toBe(gotoPage('/items-and-link/child-2')); - }); -}); diff --git a/e2e/fixtures/nav-link-items-without-suffix/package.json b/e2e/fixtures/nav-link-items-without-suffix/package.json deleted file mode 100644 index 2b33713d1..000000000 --- a/e2e/fixtures/nav-link-items-without-suffix/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "@rspress-fixture/rspress-nav-link-items-without-suffix", - "version": "1.0.0", - "private": true, - "scripts": { - "build": "rspress build", - "dev": "rspress dev", - "preview": "rspress preview" - }, - "dependencies": { - "@rspress/core": "workspace:*" - }, - "devDependencies": { - "@types/node": "^22.8.1" - } -} diff --git a/e2e/fixtures/nav-link-items-without-suffix/tsconfig.json b/e2e/fixtures/nav-link-items-without-suffix/tsconfig.json deleted file mode 100644 index 936218cee..000000000 --- a/e2e/fixtures/nav-link-items-without-suffix/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "lib": ["DOM", "ES2020"], - "module": "ESNext", - "jsx": "react-jsx", - "noEmit": true, - "strict": true, - "skipLibCheck": true, - "isolatedModules": true, - "resolveJsonModule": true, - "moduleResolution": "bundler", - "useDefineForClassFields": true, - "allowImportingTsExtensions": true - }, - "include": ["docs", "theme", "rspress.config.ts"], - "mdx": { - "checkMdx": true - } -} diff --git a/e2e/fixtures/nav-link-items/index.test.ts b/e2e/fixtures/nav-link-items/index.test.ts index adbcf9841..189ef4275 100644 --- a/e2e/fixtures/nav-link-items/index.test.ts +++ b/e2e/fixtures/nav-link-items/index.test.ts @@ -2,149 +2,237 @@ import type { Locator, Page } from '@playwright/test'; import { expect, test } from '@playwright/test'; import { getPort, killProcess, runDevCommand } from '../../utils/runCommands'; -test.describe('Nav should functions well', async () => { - let appPort: number; - let app: unknown; - let navMenu: Locator; - let navMenuItems: Locator[]; - let onlyItemsButton: Locator; - let onlyItemsContainer: Locator; - let onlyItemsChildren: Locator[]; - let itemsAndLinkButton: Locator; - let itemsAndLinkChildren: Locator[]; - let itemsAndLinkContainer: Locator; - - const init = async (page: Page) => { - await page.goto(`http://localhost:${appPort}`, { - waitUntil: 'networkidle', - }); - - // ElementHandler is currently discouraged by official - // use Locator instead - // Please refer to https://playwright.dev/docs/api/class-elementhandle - navMenu = page.locator('.rspress-nav-menu'); - navMenuItems = await page.locator('.rspress-nav-menu > *').all(); - - onlyItemsButton = navMenuItems[1].locator('.rspress-nav-menu-group-button'); - onlyItemsChildren = await navMenuItems[1] - .locator('.rspress-nav-menu-group-content a') - .all(); - onlyItemsContainer = navMenuItems[1].locator( - '.rspress-nav-menu-group-content', - ); - - itemsAndLinkButton = navMenuItems[2].locator( - '.rspress-nav-menu-group-button', - ); - itemsAndLinkChildren = await navMenuItems[2] - .locator('.rspress-nav-menu-group-content a') - .all(); - itemsAndLinkContainer = navMenuItems[2].locator( - '.rspress-nav-menu-group-content', - ); +interface NavSuiteConfig { + title: string; + configFile?: string; + paths: { + onlyLink: string; + onlyItemsItem: string; + itemsAndLink: string; + child1: string; + child2: string; }; +} + +const createNavSuite = ({ title, configFile, paths }: NavSuiteConfig) => { + test.describe(title, () => { + let appPort: number; + let app: Awaited>; + let navMenu: Locator; + let navMenuItems: Locator; + let onlyLinkItem: Locator; + let onlyItemsItem: Locator; + let itemsAndLinkItem: Locator; + let itemsAndLinkDropdown: Locator; + + const init = async (page: Page) => { + await page.goto(`http://localhost:${appPort}`, { + waitUntil: 'networkidle', + }); + + navMenu = page.locator('.rp-nav-menu'); + navMenuItems = navMenu.locator('.rp-nav-menu__item'); + + onlyLinkItem = navMenuItems.nth(0); + onlyItemsItem = navMenuItems.nth(1); + itemsAndLinkItem = navMenuItems.nth(2); + itemsAndLinkDropdown = itemsAndLinkItem.locator('.rp-hover-group'); + }; + + const getNavScreen = (page: Page) => page.locator('.rp-nav-screen'); + + const openNavScreen = async (page: Page) => { + await page.setViewportSize({ width: 500, height: 800 }); + const navScreen = getNavScreen(page); + await page.locator('.rp-nav-hamburger.rp-nav-hamburger__sm').click(); + await expect(navScreen).toHaveClass(/rp-nav-screen--open/); + await expect(navScreen).toBeVisible(); + return navScreen; + }; + + const gotoPage = (suffix: string) => `http://localhost:${appPort}${suffix}`; + + test.beforeAll(async () => { + const appDir = __dirname; + appPort = await getPort(); + app = await runDevCommand(appDir, appPort, configFile); + }); - const gotoPage = (suffix: string) => `http://localhost:${appPort}${suffix}`; - - test.beforeAll(async () => { - const appDir = __dirname; - appPort = await getPort(); - app = await runDevCommand(appDir, appPort); - }); - - test.afterAll(async () => { - if (app) { - await killProcess(app); - } - }); - - test('it should render correct visibility', async ({ page }) => { - await init(page); - - expect(await navMenu.isVisible()).toBe(true); - expect(await navMenuItems[0].isVisible()).toBe(true); - expect(await onlyItemsButton.isVisible()).toBe(true); - expect(await onlyItemsContainer.isVisible()).toBe(false); - expect(await itemsAndLinkButton.isVisible()).toBe(true); - expect(await itemsAndLinkContainer.isVisible()).toBe(false); - }); - - test('items should be visible when button is hovered', async ({ page }) => { - await init(page); - - expect(await onlyItemsContainer.isVisible()).toBe(false); - await onlyItemsButton.hover(); - expect(await onlyItemsContainer.isVisible()).toBe(true); - - expect(await itemsAndLinkContainer.isVisible()).toBe(false); - await itemsAndLinkButton.hover(); - expect(await itemsAndLinkContainer.isVisible()).toBe(true); - }); - - test('it should render correct number of nav', async ({ page }) => { - await init(page); - - expect(navMenuItems.length).toBe(3); - }); + test.afterAll(async () => { + if (app) { + await killProcess(app); + } + }); - test('it should render correct type of nav', async ({ page }) => { - await init(page); + test('it should render correct visibility', async ({ page }) => { + await init(page); + + await expect(navMenu).toBeVisible(); + await expect( + onlyLinkItem.locator('a.rp-nav-menu__item__container'), + ).toBeVisible(); + await expect( + onlyItemsItem.locator('.rp-nav-menu__item__container'), + ).toBeVisible(); + await expect( + itemsAndLinkItem.locator('.rp-nav-menu__item__container'), + ).toBeVisible(); + await expect(onlyItemsItem.locator('.rp-hover-group')).toHaveCount(1); + await expect(itemsAndLinkItem.locator('.rp-hover-group')).toHaveCount(1); + await expect(itemsAndLinkDropdown).toHaveClass(/rp-hover-group--hidden/); + }); - const onlyLinkElTag = await navMenuItems[0].evaluate(e => e.tagName); - const onlyItemsElTag = await navMenuItems[1].evaluate(e => e.tagName); - const itemsAndLinkElTag = await navMenuItems[2].evaluate(e => e.tagName); + test('items should be visible when button is hovered', async ({ page }) => { + await init(page); - expect(onlyLinkElTag).toBe('A'); - expect(onlyItemsElTag).toBe('DIV'); - expect(itemsAndLinkElTag).toBe('DIV'); - }); - - test('it should be able to redirect correctly', async ({ page }) => { - await init(page); + await expect(itemsAndLinkDropdown).toHaveClass(/rp-hover-group--hidden/); + await itemsAndLinkItem.hover(); + await expect(itemsAndLinkDropdown).not.toHaveClass( + /rp-hover-group--hidden/, + ); + }); - await navMenuItems[0].click(); - expect(page.url()).toBe(gotoPage('/only-link/index.html')); + test('it should render correct number of nav', async ({ page }) => { + await init(page); - await onlyItemsButton.hover(); - await onlyItemsChildren[0].click({ force: true }); - expect(page.url()).toBe(gotoPage('/only-items/item.html')); + await expect(navMenuItems).toHaveCount(3); + }); - await itemsAndLinkButton.click(); - expect(page.url()).toBe(gotoPage('/items-and-link/index.html')); + test('it should render correct type of nav', async ({ page }) => { + await init(page); + + const onlyLinkElTag = await onlyLinkItem.evaluate(node => node.tagName); + const onlyItemsElTag = await onlyItemsItem.evaluate(node => node.tagName); + const itemsAndLinkElTag = await itemsAndLinkItem.evaluate( + node => node.tagName, + ); + + expect(onlyLinkElTag).toBe('LI'); + expect(onlyItemsElTag).toBe('LI'); + expect(itemsAndLinkElTag).toBe('LI'); + + expect( + await onlyLinkItem.locator('a.rp-nav-menu__item__container').count(), + ).toBe(1); + expect( + await onlyItemsItem.locator('a.rp-nav-menu__item__container').count(), + ).toBe(0); + expect( + await onlyItemsItem.locator('div.rp-nav-menu__item__container').count(), + ).toBe(1); + expect( + await itemsAndLinkItem + .locator('a.rp-nav-menu__item__container') + .count(), + ).toBe(1); + }); - await page.goto(gotoPage('/'), { - waitUntil: 'networkidle', + test('it should navigate to the correct page when a link is clicked', async ({ + page, + }) => { + await init(page); + // hover OnlyItems + await onlyItemsItem.hover(); + const onlyItemsDropdown = onlyItemsItem.locator('.rp-hover-group'); + await expect(onlyItemsDropdown).not.toHaveClass(/rp-hover-group--hidden/); + await expect( + onlyItemsDropdown.locator('.rp-hover-group__item'), + ).toHaveCount(1); + + // click the first child + await onlyItemsDropdown + .locator('.rp-hover-group__item') + .first() + .locator('a') + .click(); + expect(page.url()).toBe(gotoPage(paths.onlyItemsItem)); + + // hover itemsAndLink + await itemsAndLinkItem.hover(); + await expect(itemsAndLinkDropdown).not.toHaveClass( + /rp-hover-group--hidden/, + ); + await expect( + itemsAndLinkDropdown.locator('.rp-hover-group__item'), + ).toHaveCount(2); + + // click the first child + await itemsAndLinkDropdown + .locator('.rp-hover-group__item') + .first() + .locator('a') + .click(); + expect(page.url()).toBe(gotoPage(paths.child1)); + + // click the second child + await itemsAndLinkItem.hover(); + await itemsAndLinkDropdown + .locator('.rp-hover-group__item') + .nth(1) + .locator('a') + .click(); + expect(page.url()).toBe(gotoPage(paths.child2)); }); - await itemsAndLinkButton.hover(); - await itemsAndLinkChildren[0].click({ force: true, timeout: 1000 }); - expect(page.url()).toBe(gotoPage('/items-and-link/child-1.html')); - await page.goto(gotoPage('/'), { - waitUntil: 'networkidle', + test('it should navigate to the correct page when a link is clicked - navScreen', async ({ + page, + }) => { + await init(page); + + // Mobile menu: first ensure top-level links open expected pages. + await onlyLinkItem.locator('a').click(); + expect(page.url()).toBe(gotoPage(paths.onlyLink)); + + await page.goto(gotoPage('/'), { waitUntil: 'networkidle' }); + let navScreen = await openNavScreen(page); + await navScreen + .locator('.rp-nav-screen-menu-item') + .filter({ hasText: 'OnlyItems' }) + .click(); + // Mobile submenu: OnlyItems -> Item. + await navScreen + .locator('a.rp-nav-screen-menu-item') + .filter({ hasText: 'Item' }) + .click(); + expect(page.url()).toBe(gotoPage(paths.onlyItemsItem)); + + await page.goto(gotoPage('/'), { + waitUntil: 'networkidle', + }); + navScreen = await openNavScreen(page); + await navScreen + .locator('.rp-nav-screen-menu-item') + .filter({ hasText: 'ItemsAndLink' }) + .click(); + // Mobile submenu: ItemsAndLink -> Child1. + await navScreen + .locator('a.rp-nav-screen-menu-item') + .filter({ hasText: 'Child1' }) + .click(); + expect(page.url()).toBe(gotoPage(paths.child1)); }); - await itemsAndLinkButton.hover(); - await itemsAndLinkChildren[1].click({ force: true, timeout: 1000 }); - expect(page.url()).toBe(gotoPage('/items-and-link/child-2.html')); }); +}; + +createNavSuite({ + title: 'Nav should functions well', + paths: { + onlyLink: '/only-link/index.html', + onlyItemsItem: '/only-items/item.html', + itemsAndLink: '/items-and-link/index.html', + child1: '/items-and-link/child-1.html', + child2: '/items-and-link/child-2.html', + }, +}); - test('it should render correct text', async ({ page }) => { - await init(page); - - const onlyLinkText = await navMenuItems[0].textContent(); - const onlyItemsButtonText = await onlyItemsButton.textContent(); - const onlyItemsChildrenText = await onlyItemsChildren[0].textContent(); - - const itemsAndLinkButtonText = await itemsAndLinkButton.textContent(); - const itemsAndLinkChildrenText1 = - await itemsAndLinkChildren[0].textContent(); - const itemsAndLinkChildrenText2 = - await itemsAndLinkChildren[1].textContent(); - - expect(onlyLinkText).toBe('OnlyLink'); - expect(onlyItemsButtonText).toBe('OnlyItems'); - expect(onlyItemsChildrenText).toBe('Item'); - expect(itemsAndLinkButtonText).toBe('ItemsAndLink'); - expect(itemsAndLinkChildrenText1).toBe('Child1'); - expect(itemsAndLinkChildrenText2).toBe('Child2'); - }); +createNavSuite({ + title: 'Nav should functions well with clean urls', + configFile: 'rspress-clean.config.ts', + paths: { + onlyLink: '/only-link/', + onlyItemsItem: '/only-items/item', + itemsAndLink: '/items-and-link/', + child1: '/items-and-link/child-1', + child2: '/items-and-link/child-2', + }, }); diff --git a/e2e/fixtures/nav-link-items-without-suffix/rspress.config.ts b/e2e/fixtures/nav-link-items/rspress-clean.config.ts similarity index 100% rename from e2e/fixtures/nav-link-items-without-suffix/rspress.config.ts rename to e2e/fixtures/nav-link-items/rspress-clean.config.ts diff --git a/e2e/fixtures/nav-link/index.test.ts b/e2e/fixtures/nav-link/index.test.ts index da146557d..2a175c73c 100644 --- a/e2e/fixtures/nav-link/index.test.ts +++ b/e2e/fixtures/nav-link/index.test.ts @@ -6,7 +6,7 @@ import { getPort, killProcess, runDevCommand } from '../../utils/runCommands'; test.describe('Navigation with ', async () => { let appPort: number; - let app: unknown; + let app: Awaited>; const getContext = async (page: Page) => { await page.goto(`http://localhost:${appPort}`, { @@ -17,7 +17,7 @@ test.describe('Navigation with ', async () => { return { page, - anchor: page.locator('.rspress-nav-menu > a:first-child').first(), + anchor: page.locator('.rp-nav-menu__item a').first(), shouldOpenNewPage, dispose, }; diff --git a/e2e/fixtures/nested-overview/index.test.ts b/e2e/fixtures/nested-overview/index.test.ts index 71e8c1ddc..afe1d9358 100644 --- a/e2e/fixtures/nested-overview/index.test.ts +++ b/e2e/fixtures/nested-overview/index.test.ts @@ -2,8 +2,8 @@ import { expect, test } from '@playwright/test'; import { getPort, killProcess, runDevCommand } from '../../utils/runCommands'; test.describe('Nested overview page', async () => { - let appPort; - let app; + let appPort: number; + let app: Awaited>; test.beforeAll(async () => { const appDir = __dirname; appPort = await getPort(); @@ -25,14 +25,15 @@ test.describe('Nested overview page', async () => { waitUntil: 'networkidle', }, ); + const overviewHeadings = page.locator( + '.rp-overview h2.rspress-doc-outline span', + ); + await expect(overviewHeadings).toHaveText(['Level 2']); - const h2 = await page.$$('.overview-index h2'); - const h2Texts = await Promise.all(h2.map(element => element.textContent())); - expect(h2Texts.join(',')).toEqual(['Level 2'].join(',')); - - const h3 = await page.$$('.overview-group_f8331 h3'); - const h3Texts = await Promise.all(h3.map(element => element.textContent())); - expect(h3Texts.join(',')).toEqual(['Level 2', 'two', 'Level 3'].join(',')); + const overviewGroups = page.locator( + '.rp-overview .rp-overview-group__item__title > a', + ); + await expect(overviewGroups).toHaveText(['Level 2', 'two', 'Level 3']); }); test('Should load nested overview page correctly - level 2', async ({ @@ -44,14 +45,15 @@ test.describe('Nested overview page', async () => { waitUntil: 'networkidle', }, ); + const overviewHeadings = page.locator( + '.rp-overview h2.rspress-doc-outline span', + ); + await expect(overviewHeadings).toHaveText(['two', 'Level 3']); - const h2 = await page.$$('.overview-index h2'); - const h2Texts = await Promise.all(h2.map(element => element.textContent())); - expect(h2Texts.join(',')).toEqual(['two', 'Level 3'].join(',')); - - const h3 = await page.$$('.overview-group_f8331 h3'); - const h3Texts = await Promise.all(h3.map(element => element.textContent())); - expect(h3Texts.join(',')).toEqual(['two', 'Level 3', 'three'].join(',')); + const overviewGroups = page.locator( + '.rp-overview .rp-overview-group__item__title > a', + ); + await expect(overviewGroups).toHaveText(['two', 'Level 3', 'three']); }); test('Should load nested overview page correctly - level 3', async ({ @@ -63,13 +65,14 @@ test.describe('Nested overview page', async () => { waitUntil: 'networkidle', }, ); + const overviewHeadings = page.locator( + '.rp-overview h2.rspress-doc-outline span', + ); + await expect(overviewHeadings).toHaveText(['three']); - const h2 = await page.$$('.overview-index h2'); - const h2Texts = await Promise.all(h2.map(element => element.textContent())); - expect(h2Texts.join(',')).toEqual(['three'].join(',')); - - const h3 = await page.$$('.overview-group_f8331 h3'); - const h3Texts = await Promise.all(h3.map(element => element.textContent())); - expect(h3Texts.join(',')).toEqual(['three'].join(',')); + const overviewGroups = page.locator( + '.rp-overview .rp-overview-group__item__title > a', + ); + await expect(overviewGroups).toHaveText(['three']); }); }); diff --git a/e2e/fixtures/no-config-root/index.test.ts b/e2e/fixtures/no-config-root/index.test.ts index 37dafcdcb..5387d1fdc 100644 --- a/e2e/fixtures/no-config-root/index.test.ts +++ b/e2e/fixtures/no-config-root/index.test.ts @@ -24,9 +24,8 @@ test.describe('no config.root dev test', async () => { test('Index page', async ({ page }) => { await page.goto(`http://localhost:${appPort}`); - const h1 = await page.$('h1'); - const text = await page.evaluate(h1 => h1?.textContent, h1); - expect(text).toContain('Hello world'); + const h1 = page.locator('h1'); + await expect(h1).toContainText('Hello world'); }); }); @@ -50,8 +49,7 @@ test.describe('no config.root build and preview test', async () => { await page.goto(`http://localhost:${appPort}`, { waitUntil: 'networkidle', }); - const h1 = await page.$('h1'); - const text = await page.evaluate(h1 => h1?.textContent, h1); - expect(text).toContain('Hello world'); + const h1 = page.locator('h1'); + await expect(h1).toContainText('Hello world'); }); }); diff --git a/e2e/fixtures/package-manager-tabs/index.test.ts b/e2e/fixtures/package-manager-tabs/index.test.ts index 7df8d34ad..f8acd0b45 100644 --- a/e2e/fixtures/package-manager-tabs/index.test.ts +++ b/e2e/fixtures/package-manager-tabs/index.test.ts @@ -2,8 +2,8 @@ import { expect, test } from '@playwright/test'; import { getPort, killProcess, runDevCommand } from '../../utils/runCommands'; test.describe('tabs-component test', async () => { - let appPort; - let app; + let appPort: number; + let app: Awaited>; test.beforeAll(async () => { const appDir = __dirname; @@ -20,11 +20,9 @@ test.describe('tabs-component test', async () => { test('Index page', async ({ page }) => { await page.goto(`http://localhost:${appPort}`); - await page.waitForSelector('[class^="tab_"] > div > span'); - const tabs = await page.$$('[class^="tab_"] > div > span'); - const tabsText = await Promise.all( - tabs.map(element => element.textContent()), - ); + await page.waitForSelector('.rp-tabs__tab'); + const tabs = page.locator('.rp-tabs__tab'); + const tabsText = (await tabs.allInnerTexts()).map(text => text.trim()); expect(tabsText).toEqual([ 'npm', @@ -41,62 +39,38 @@ test.describe('tabs-component test', async () => { 'bun', ]); - const clickTabs = await page.$$('[class^="tab_"]'); + const clickTabs = tabs; + const getCommands = async () => + (await page.locator('.rp-codeblock_content code').allInnerTexts()).map( + text => text.trim(), + ); - await clickTabs[0].click(); - const npmSpanElements = await page.$$('code > span > span'); - const npmCode = await Promise.all( - npmSpanElements.map(element => element.textContent()), - ); - expect(npmCode).toEqual([ - 'npm', - ' create rspress@latest', - 'npm', - ' install rspress -D', - 'npx', - ' example-cli-tool --yes', + await clickTabs.nth(0).click(); + expect(await getCommands()).toEqual([ + 'npm create rspress@latest', + 'npm install rspress -D', + 'npx example-cli-tool --yes', ]); - await clickTabs[1].click(); - const yarnSpanElements = await page.$$('code > span > span'); - const yarnCode = await Promise.all( - yarnSpanElements.map(element => element.textContent()), - ); - expect(yarnCode).toEqual([ - 'yarn', - ' create rspress', - 'yarn', - ' add rspress -D', - 'yarn', - ' example-cli-tool --yes', + await clickTabs.nth(1).click(); + expect(await getCommands()).toEqual([ + 'yarn create rspress', + 'yarn add rspress -D', + 'yarn example-cli-tool --yes', ]); - await clickTabs[2].click(); - const pnpmSpanElements = await page.$$('code > span > span'); - const pnpmCode = await Promise.all( - pnpmSpanElements.map(element => element.textContent()), - ); - expect(pnpmCode).toEqual([ - 'pnpm', - ' create rspress@latest', - 'pnpm', - ' add rspress -D', - 'pnpm', - ' example-cli-tool --yes', + await clickTabs.nth(2).click(); + expect(await getCommands()).toEqual([ + 'pnpm create rspress@latest', + 'pnpm add rspress -D', + 'pnpm example-cli-tool --yes', ]); - await clickTabs[3].click(); - const bunSpanElements = await page.$$('code > span > span'); - const bunCode = await Promise.all( - bunSpanElements.map(element => element.textContent()), - ); - expect(bunCode).toEqual([ - 'bun', - ' create rspress@latest', - 'bun', - ' add rspress -D', - 'bun', - ' example-cli-tool --yes', + await clickTabs.nth(3).click(); + expect(await getCommands()).toEqual([ + 'bun create rspress@latest', + 'bun add rspress -D', + 'bun example-cli-tool --yes', ]); }); }); diff --git a/e2e/fixtures/page-type-home/index.test.ts b/e2e/fixtures/page-type-home/index.test.ts index 7301f05ef..d699352b4 100644 --- a/e2e/fixtures/page-type-home/index.test.ts +++ b/e2e/fixtures/page-type-home/index.test.ts @@ -19,17 +19,17 @@ test.describe('home pageType', async () => { test('Hero', async ({ page }) => { await page.goto(`http://localhost:${appPort}/base/`); - await expect(page.locator('h1').textContent()).resolves.toBe( + await expect(page.locator('.rp-home-hero__title-brand')).toHaveText( 'E2E case title', ); - await expect( - page.locator('.rspress-home-hero-text').textContent(), - ).resolves.toBe('E2E case subTitle'); - await expect( - page.locator('.rspress-home-hero-tagline').textContent(), - ).resolves.toBe('E2E case tagline'); + await expect(page.locator('.rp-home-hero__subtitle')).toHaveText( + 'E2E case subTitle', + ); + await expect(page.locator('.rp-home-hero__tagline')).toHaveText( + 'E2E case tagline', + ); - const img = page.locator('.rspress-home-hero-image img').first(); + const img = page.locator('.rp-home-hero__image img').first(); await expect(img.getAttribute('src')).resolves.toBe('/base/brand.png'); await expect(img.getAttribute('alt')).resolves.toBe('E2E case brand image'); await expect(img.getAttribute('srcset')).resolves.toBe( @@ -39,11 +39,11 @@ test.describe('home pageType', async () => { '((min-width: 50em) and (max-width: 60em)) 50em, (max-width: 30em) 20em', ); - const actions = page.locator('.rspress-home-hero-actions a'); + const actions = page.locator('.rp-home-hero__actions a'); await expect(actions).toHaveCount(3); - await expect(actions.nth(0).textContent()).resolves.toBe('Action 1'); - await expect(actions.nth(1).textContent()).resolves.toBe('Action 2'); - await expect(actions.nth(2).textContent()).resolves.toBe('External'); + await expect(actions.nth(0)).toHaveText('Action 1'); + await expect(actions.nth(1)).toHaveText('Action 2'); + await expect(actions.nth(2)).toHaveText('External'); // click the first action const url1 = page.url(); await actions.nth(0).click(); @@ -53,17 +53,17 @@ test.describe('home pageType', async () => { test('Hero - zh', async ({ page }) => { await page.goto(`http://localhost:${appPort}/base/zh/`); - await expect(page.locator('h1').textContent()).resolves.toBe( + await expect(page.locator('.rp-home-hero__title-brand')).toHaveText( 'E2E 用例 title', ); - await expect( - page.locator('.rspress-home-hero-text').textContent(), - ).resolves.toBe('E2E 用例 subTitle'); - await expect( - page.locator('.rspress-home-hero-tagline').textContent(), - ).resolves.toBe('E2E 用例 tagline'); + await expect(page.locator('.rp-home-hero__subtitle')).toHaveText( + 'E2E 用例 subTitle', + ); + await expect(page.locator('.rp-home-hero__tagline')).toHaveText( + 'E2E 用例 tagline', + ); - const img = page.locator('.rspress-home-hero-image img').first(); + const img = page.locator('.rp-home-hero__image img').first(); await expect(img.getAttribute('src')).resolves.toBe('/base/brand.png'); await expect(img.getAttribute('alt')).resolves.toBe('E2E 用例 brand image'); await expect(img.getAttribute('srcset')).resolves.toBe( @@ -73,11 +73,11 @@ test.describe('home pageType', async () => { '((min-width: 50em) and (max-width: 60em)) 50em, (max-width: 30em) 20em', ); - const actions = page.locator('.rspress-home-hero-actions a'); + const actions = page.locator('.rp-home-hero__actions a'); await expect(actions).toHaveCount(3); - await expect(actions.nth(0).textContent()).resolves.toBe('操作 1'); - await expect(actions.nth(1).textContent()).resolves.toBe('操作 2'); - await expect(actions.nth(2).textContent()).resolves.toBe('External'); + await expect(actions.nth(0)).toHaveText('操作 1'); + await expect(actions.nth(1)).toHaveText('操作 2'); + await expect(actions.nth(2)).toHaveText('External'); // click the first action const url1 = page.url(); await actions.nth(0).click(); @@ -87,14 +87,14 @@ test.describe('home pageType', async () => { test('Features', async ({ page }) => { await page.goto(`http://localhost:${appPort}/base/`); - const features = await page.$$('.rspress-home-feature-card'); - expect(features).toHaveLength(2); + const features = page.locator('.rp-home-feature__card'); + await expect(features).toHaveCount(2); const url1 = page.url(); - await features[0].click(); + await features.nth(0).click(); expect(page.url()).toBe(url1); - await features[1].click(); + await features.nth(1).click(); expect(page.url()).toBe('https://example.com/'); }); }); diff --git a/e2e/fixtures/plugin-api-docgen/index.test.ts b/e2e/fixtures/plugin-api-docgen/index.test.ts index d35b61513..6f1f2c892 100644 --- a/e2e/fixtures/plugin-api-docgen/index.test.ts +++ b/e2e/fixtures/plugin-api-docgen/index.test.ts @@ -3,8 +3,8 @@ import { getPort, killProcess, runDevCommand } from '../../utils/runCommands'; import { searchInPage } from '../../utils/search'; test.describe('api-docgen test', async () => { - let appPort; - let app; + let appPort: number; + let app: unknown; test.beforeAll(async () => { const appDir = __dirname; @@ -20,34 +20,33 @@ test.describe('api-docgen test', async () => { test('Index page', async ({ page }) => { await page.goto(`http://localhost:${appPort}`); - await page.waitForSelector('table'); - const tableH3 = await page.$('#button'); - expect(tableH3).toBeTruthy(); + await page.waitForSelector('.rspress-plugin-api-docgen table'); + const tableH3 = page.locator('#button'); + await expect(tableH3).toBeVisible(); - const table = await page.$('table'); - const tableContent = await page.evaluate(table => table?.innerHTML, table); + const table = page.locator('.rspress-plugin-api-docgen table'); // Property - expect(tableContent).toContain('Property'); - expect(tableContent).toContain('disabled'); - expect(tableContent).toContain('size'); + await expect(table).toContainText('Property'); + await expect(table).toContainText('disabled'); + await expect(table).toContainText('size'); // Description - expect(tableContent).toContain('Description'); - expect(tableContent).toContain('Whether to disable the button'); - expect(tableContent).toContain('- This is extra line a'); - expect(tableContent).toContain('- This is extra line b'); - expect(tableContent).toContain('Type of Button'); + await expect(table).toContainText('Description'); + await expect(table).toContainText('Whether to disable the button'); + await expect(table).toContainText('- This is extra line a'); + await expect(table).toContainText('- This is extra line b'); + await expect(table).toContainText('Type of Button'); // Type - expect(tableContent).toContain('Type'); - expect(tableContent).toContain('boolean'); - expect(tableContent).toContain('"mini" | "small" | "default" | "large"'); + await expect(table).toContainText('Type'); + await expect(table).toContainText('boolean'); + await expect(table).toContainText('"mini" | "small" | "default" | "large"'); // Default Value - expect(tableContent).toContain('Default Value'); - expect(tableContent).toContain('-'); - expect(tableContent).toContain("'default'"); + await expect(table).toContainText('Default Value'); + await expect(table).toContainText('-'); + await expect(table).toContainText("'default'"); }); test('search index should include api-docgen result', async ({ page }) => { diff --git a/e2e/fixtures/plugin-playground/index.test.ts b/e2e/fixtures/plugin-playground/index.test.ts index 25dc3539a..7f07a4ad0 100644 --- a/e2e/fixtures/plugin-playground/index.test.ts +++ b/e2e/fixtures/plugin-playground/index.test.ts @@ -21,23 +21,23 @@ test.describe('plugin test', async () => { waitUntil: 'networkidle', }); - const playgroundElements = await page.$$('.rspress-playground'); + const playgroundElements = page.locator('.rspress-playground'); + await expect(playgroundElements).toHaveCount(3); - const internalDemoCodePreviewDefault = await page + const internalDemoCodePreviewDefault = page .locator('.rspress-playground > .rspress-playground-runner > div') .getByText('Hello World Internal (default)'); - const internalDemoCodePreviewVertical = await page + const internalDemoCodePreviewVertical = page .locator('.rspress-playground > .rspress-playground-runner > div') .getByText('Hello World Internal (vertical)'); - const externalDemoCodePreview = await page + const externalDemoCodePreview = page .locator('.rspress-playground > .rspress-playground-runner > div') .getByText('Hello World External'); - expect(playgroundElements.length).toBe(3); - expect(await internalDemoCodePreviewDefault.count()).toBe(1); - expect(await internalDemoCodePreviewVertical.count()).toBe(1); - expect(await externalDemoCodePreview.count()).toBe(1); + await expect(internalDemoCodePreviewDefault).toHaveCount(1); + await expect(internalDemoCodePreviewVertical).toHaveCount(1); + await expect(externalDemoCodePreview).toHaveCount(1); }); }); diff --git a/e2e/fixtures/plugin-preview-custom-entry/index.test.ts b/e2e/fixtures/plugin-preview-custom-entry/index.test.ts index 89b04edfb..e9acaa6fc 100644 --- a/e2e/fixtures/plugin-preview-custom-entry/index.test.ts +++ b/e2e/fixtures/plugin-preview-custom-entry/index.test.ts @@ -20,7 +20,8 @@ test.describe('plugin test', async () => { await page.goto(`http://localhost:${appPort}/`, { waitUntil: 'networkidle', }); - const codeBlockElements = await page.$$('.rspress-doc > .rspress-preview'); + const codeBlockElements = page.locator('.rspress-doc > .rspress-preview'); + await expect(codeBlockElements).toHaveCount(4); const internalIframeJsxDemoCodePreview = await page .frameLocator('iframe') @@ -43,7 +44,6 @@ test.describe('plugin test', async () => { .getByText('VUE') .innerText(); - expect(codeBlockElements.length).toBe(4); expect(internalIframeJsxDemoCodePreview).toBe('Hello World JSX'); expect(internalIframeTsxDemoCodePreview).toBe('Hello World TSX'); expect(externalIframeJsxDemoCodePreview).toBe('Hello World External'); diff --git a/e2e/fixtures/plugin-preview/index.test.ts b/e2e/fixtures/plugin-preview/index.test.ts index 5f3d88ec5..28394fb94 100644 --- a/e2e/fixtures/plugin-preview/index.test.ts +++ b/e2e/fixtures/plugin-preview/index.test.ts @@ -20,9 +20,10 @@ test.describe('plugin test', async () => { await page.goto(`http://localhost:${appPort}/`, { waitUntil: 'networkidle', }); - const codeBlockElements = await page.$$( + const codeBlockElements = page.locator( '.rspress-doc > div[class*=language-]', ); + await expect(codeBlockElements).toHaveCount(3); const internalDemoCodePreview = await page .frameLocator('iframe') @@ -37,7 +38,6 @@ test.describe('plugin test', async () => { .getByText('JSON') .innerText(); - expect(codeBlockElements.length).toBe(3); expect(internalDemoCodePreview).toBe('Hello World Internal'); expect(externalDemoCodePreview).toBe('Hello World External'); expect(transformedCodePreview).toBe('Render from JSON'); diff --git a/e2e/fixtures/plugin-shiki/index.test.ts b/e2e/fixtures/plugin-shiki/index.test.ts index e5c960645..3af6d3e78 100644 --- a/e2e/fixtures/plugin-shiki/index.test.ts +++ b/e2e/fixtures/plugin-shiki/index.test.ts @@ -26,22 +26,26 @@ test.describe('plugin shiki test', async () => { await page.goto(`http://localhost:${appPort}`, { waitUntil: 'networkidle', }); - const shikiDoms = await page.$$('.rspress-code-content'); - expect(shikiDoms.length).toBe(6); + const shikiDoms = page.locator('.rp-codeblock_content'); + await expect(shikiDoms).toHaveCount(6); - const firstShikiDom = shikiDoms[0]; + const firstShikiDom = shikiDoms.first(); expect( - await firstShikiDom.$eval('code', node => - node.computedStyleMap().get('white-space')?.toString(), - ), + await firstShikiDom + .locator('code') + .evaluate(node => + node.computedStyleMap().get('white-space')?.toString(), + ), ).toBe('pre'); - await firstShikiDom.$eval('button', btn => btn.click()); + await firstShikiDom.locator('button[title="Toggle code wrap"]').click(); expect( - await firstShikiDom.$eval('code', node => - node.computedStyleMap().get('white-space')?.toString(), - ), + await firstShikiDom + .locator('code') + .evaluate(node => + node.computedStyleMap().get('white-space')?.toString(), + ), ).toBe('pre-wrap'); }); @@ -49,7 +53,7 @@ test.describe('plugin shiki test', async () => { await page.goto(`http://localhost:${appPort}/langAlias`, { waitUntil: 'networkidle', }); - const shikiDoms = await page.$$('.rspress-code-content'); - expect(shikiDoms.length).toBe(1); + const shikiDoms = page.locator('.rp-codeblock_content'); + await expect(shikiDoms).toHaveCount(1); }); }); diff --git a/e2e/fixtures/plugin-twoslash/index.test.ts b/e2e/fixtures/plugin-twoslash/index.test.ts index 61003ebed..e2562b243 100644 --- a/e2e/fixtures/plugin-twoslash/index.test.ts +++ b/e2e/fixtures/plugin-twoslash/index.test.ts @@ -27,20 +27,22 @@ test.describe('plugin twoslash test', async () => { waitUntil: 'networkidle', }); - const triggers = await page.$$('twoslash-popup-trigger'); - expect(triggers.length).toBe(10); + const triggers = page.locator('twoslash-popup-trigger'); + await expect(triggers).toHaveCount(10); - for (const trigger of triggers) { - const container = await trigger.$('twoslash-popup-container'); - expect(container).not.toBeNull(); + const count = await triggers.count(); + for (let i = 0; i < count; i++) { + const trigger = triggers.nth(i); + const container = trigger.locator('twoslash-popup-container'); + await expect(container).toBeAttached(); - const initialized = await container?.getAttribute('data-initialized'); + const initialized = await container.getAttribute('data-initialized'); expect(initialized).toBeNull(); - const inner = await container.$('.twoslash-popup-inner'); - expect(inner).not.toBeNull(); - const arrow = await container.$('.twoslash-popup-arrow'); - expect(arrow).not.toBeNull(); + const inner = container.locator('.twoslash-popup-inner'); + await expect(inner).toBeAttached(); + const arrow = container.locator('.twoslash-popup-arrow'); + await expect(arrow).toBeAttached(); } }); @@ -49,26 +51,32 @@ test.describe('plugin twoslash test', async () => { waitUntil: 'networkidle', }); - const portal = await page.$('twoslash-popup-portal'); - expect(portal).not.toBeNull(); + const portal = page.locator('twoslash-popup-portal'); + await expect(portal).toBeAttached(); - const containers = await portal.$$('twoslash-popup-container'); - expect(containers.length).toBe(10); + const containers = portal.locator('twoslash-popup-container'); + await expect(containers).toHaveCount(10); - const extractTypeAlways = await containers[1].getAttribute('data-always'); + const extractTypeAlways = await containers + .nth(1) + .getAttribute('data-always'); expect(extractTypeAlways).toBe('true'); - const completionsAlways = await containers[4].getAttribute('data-always'); + const completionsAlways = await containers + .nth(4) + .getAttribute('data-always'); expect(completionsAlways).toBe('true'); - for (const container of containers) { + const count = await containers.count(); + for (let i = 0; i < count; i++) { + const container = containers.nth(i); const initialized = await container.getAttribute('data-initialized'); expect(initialized).not.toBeNull(); - const inner = await container.$('.twoslash-popup-inner'); - expect(inner).not.toBeNull(); - const arrow = await container.$('.twoslash-popup-arrow'); - expect(arrow).not.toBeNull(); + const inner = container.locator('.twoslash-popup-inner'); + await expect(inner).toBeAttached(); + const arrow = container.locator('.twoslash-popup-arrow'); + await expect(arrow).toBeAttached(); } }); @@ -77,13 +85,13 @@ test.describe('plugin twoslash test', async () => { waitUntil: 'networkidle', }); - const codeBlock = await page.$( + const codeBlock = page.locator( 'h2:has-text("Highlighting") + .language-ts', ); - expect(codeBlock).not.toBeNull(); + await expect(codeBlock).toBeAttached(); - const highlighted = await codeBlock?.$('.twoslash-highlighted'); - expect(highlighted).not.toBeNull(); + const highlighted = codeBlock.locator('.twoslash-highlighted'); + await expect(highlighted).toBeAttached(); }); test('should show errors in code blocks with twoslash', async ({ page }) => { @@ -91,14 +99,14 @@ test.describe('plugin twoslash test', async () => { waitUntil: 'networkidle', }); - const codeBlock = await page.$('h2:has-text("Error") + .language-ts'); - expect(codeBlock).not.toBeNull(); + const codeBlock = page.locator('h2:has-text("Error") + .language-ts'); + await expect(codeBlock).toBeAttached(); - const error = await codeBlock?.$('.twoslash-error'); - expect(error).not.toBeNull(); + const error = codeBlock.locator('.twoslash-error'); + await expect(error).toBeAttached(); - const errorLine = await codeBlock?.$('.twoslash-error-line'); - expect(errorLine).not.toBeNull(); + const errorLine = codeBlock.locator('.twoslash-error-line'); + await expect(errorLine).toBeAttached(); }); test('should not apply twoslash to code blocks without twoslash', async ({ @@ -108,12 +116,12 @@ test.describe('plugin twoslash test', async () => { waitUntil: 'networkidle', }); - const codeBlock = await page.$( + const codeBlock = page.locator( 'h2:has-text("Disable twoslash") + .language-ts', ); - expect(codeBlock).not.toBeNull(); + await expect(codeBlock).toBeAttached(); - const triggers = await codeBlock?.$$('twoslash-popup-trigger'); - expect(triggers.length).toBe(0); + const triggers = codeBlock.locator('twoslash-popup-trigger'); + await expect(triggers).toHaveCount(0); }); }); diff --git a/e2e/fixtures/plugin-typedoc/index.test.ts b/e2e/fixtures/plugin-typedoc/index.test.ts index 919561272..ed4efd6c6 100644 --- a/e2e/fixtures/plugin-typedoc/index.test.ts +++ b/e2e/fixtures/plugin-typedoc/index.test.ts @@ -4,8 +4,8 @@ import { getNavbarItems, getSidebarTexts } from '../../utils/getSideBar'; import { getPort, killProcess, runDevCommand } from '../../utils/runCommands'; test.describe('plugin-typedoc single entry', async () => { - let appPort; - let app; + let appPort: number; + let app: Awaited>; test.beforeAll(async () => { const appDir = path.join(__dirname, 'single'); appPort = await getPort(); @@ -22,24 +22,27 @@ test.describe('plugin-typedoc single entry', async () => { waitUntil: 'networkidle', }); - const navItems = await getNavbarItems(page); - expect(navItems?.length).toBe(2); + const navItems = getNavbarItems(page); + await expect(navItems).toHaveCount(2); const sidebarTexts = await getSidebarTexts(page); - expect(sidebarTexts?.length).toBe(3); + expect(sidebarTexts.length).toBe(6); expect(sidebarTexts.join(',')).toEqual( [ '@rspress-fixture/rspress-plugin-typedoc-single', - 'FunctionsFunction: createMiddlewareFunction: mergeMiddlewares', - 'TypesType alias: Middleware', + 'Functions', + 'Function: createMiddleware', + 'Function: mergeMiddlewares', + 'Types', + 'Type alias: Middleware', ].join(','), ); }); }); test.describe('plugin-typedoc multi entries', async () => { - let appPort; - let app; + let appPort: number; + let app: Awaited>; test.beforeAll(async () => { const appDir = path.join(__dirname, 'multi'); appPort = await getPort(); @@ -56,17 +59,23 @@ test.describe('plugin-typedoc multi entries', async () => { waitUntil: 'networkidle', }); - const navItems = await getNavbarItems(page); - expect(navItems?.length).toBe(2); + const navItems = getNavbarItems(page); + await expect(navItems).toHaveCount(2); const sidebarTexts = await getSidebarTexts(page); - expect(sidebarTexts?.length).toBe(4); + expect(sidebarTexts.length).toBe(10); expect(sidebarTexts.join(',')).toEqual( [ '@rspress-fixture/rspress-plugin-typedoc-multi', - 'FunctionsFunction: createMiddlewareFunction: mergeMiddlewaresFunction: getRspressUrl', - 'ModulesModule: middlewareModule: raw-link', - 'TypesType alias: Middleware', + 'Functions', + 'Function: createMiddleware', + 'Function: mergeMiddlewares', + 'Function: getRspressUrl', + 'Modules', + 'Module: middleware', + 'Module: raw-link', + 'Types', + 'Type alias: Middleware', ].join(','), ); }); diff --git a/e2e/fixtures/production/index.test.ts b/e2e/fixtures/production/index.test.ts index f9a685e5d..2b66c5792 100644 --- a/e2e/fixtures/production/index.test.ts +++ b/e2e/fixtures/production/index.test.ts @@ -26,16 +26,13 @@ test.describe('basic test', async () => { await page.goto(`http://localhost:${appPort}`, { waitUntil: 'networkidle', }); - const darkModeButton = await page.$('.rspress-nav-appearance'); - const html = await page.$('html'); - let htmlClass = await page.evaluate( - html => html?.getAttribute('class'), - html, - ); - const defaultMode = htmlClass?.includes('dark') ? 'dark' : 'light'; - await darkModeButton?.click(); - // check the class in html - htmlClass = await page.evaluate(html => html?.getAttribute('class'), html); - expect(htmlClass?.includes('dark')).toBe(defaultMode !== 'dark'); + const appearanceToggle = page.locator('.rp-switch-appearance').first(); + const getIsDark = () => + page.evaluate(() => + document.documentElement.classList.contains('rp-dark'), + ); + const defaultIsDark = await getIsDark(); + await appearanceToggle.click(); + await expect.poll(getIsDark).toBe(!defaultIsDark); }); }); diff --git a/e2e/fixtures/public-dir/index.test.ts b/e2e/fixtures/public-dir/index.test.ts index 132dea269..ac77fa61b 100644 --- a/e2e/fixtures/public-dir/index.test.ts +++ b/e2e/fixtures/public-dir/index.test.ts @@ -55,9 +55,8 @@ test.describe('basic test', async () => { waitUntil: 'networkidle', }); - const img = await page.$('.rspress-doc img'); - const src = await img?.getAttribute('src'); - expect(src).toEqual('/base/rspress-icon.png'); + const img = page.locator('.rspress-doc img'); + await expect(img).toHaveAttribute('src', '/base/rspress-icon.png'); }); test('should load public dir img successfully under "rspress dev"', async ({ @@ -70,8 +69,7 @@ test.describe('basic test', async () => { waitUntil: 'networkidle', }); - const img = await page.$('.rspress-doc img'); - const src = await img?.getAttribute('src'); - expect(src).toEqual('/base/rspress-icon.png'); + const img = page.locator('.rspress-doc img'); + await expect(img).toHaveAttribute('src', '/base/rspress-icon.png'); }); }); diff --git a/e2e/fixtures/react-19/index.test.ts b/e2e/fixtures/react-19/index.test.ts index 380881485..2d1799c4f 100644 --- a/e2e/fixtures/react-19/index.test.ts +++ b/e2e/fixtures/react-19/index.test.ts @@ -18,9 +18,8 @@ test.describe('React 19 test', async () => { test('Index page', async ({ page }) => { await page.goto(`http://localhost:${appPort}`); - const h1 = await page.$('h1'); - const text = await page.evaluate(h1 => h1?.textContent, h1); - expect(text).toContain('Hello world'); + const h1 = page.locator('h1'); + await expect(h1).toContainText('Hello world'); }); test('404 page', async ({ page }) => { @@ -28,7 +27,6 @@ test.describe('React 19 test', async () => { waitUntil: 'networkidle', }); // find the 404 text in the page - const text = await page.evaluate(() => document.body.textContent); - expect(text).toContain('404'); + await expect(page.locator('body')).toContainText('404'); }); }); diff --git a/e2e/fixtures/replace-rules/index.test.ts b/e2e/fixtures/replace-rules/index.test.ts index 9f8ff6510..8dd20510b 100644 --- a/e2e/fixtures/replace-rules/index.test.ts +++ b/e2e/fixtures/replace-rules/index.test.ts @@ -2,8 +2,8 @@ import { expect, test } from '@playwright/test'; import { getPort, killProcess, runDevCommand } from '../../utils/runCommands'; test.describe('replace-rules test', async () => { - let appPort; - let app; + let appPort: number; + let app: Awaited>; test.beforeAll(async () => { const appDir = __dirname; appPort = await getPort(); @@ -20,37 +20,29 @@ test.describe('replace-rules test', async () => { await page.goto(`http://localhost:${appPort}`); // replace text in _meta.json - const nav = await page.$('.rspress-nav-menu'); - const navContent = await page.evaluate(nav => nav?.textContent, nav); + const navItem = page.locator('.rp-nav-menu__item__container').nth(0); + await expect(navItem).toHaveText('bar-meta'); // replace text in object frontmatter - const hero = await page.$('h1'); - const heroContent = await page.evaluate(hero => hero?.textContent, hero); - - expect(navContent).toEqual('bar-meta'); - expect(heroContent).toEqual('bar-hero'); + const hero = page.locator('.rp-home-hero__title-brand'); + await expect(hero).toHaveText('bar-hero'); }); test('Foo page', async ({ page }) => { await page.goto(`http://localhost:${appPort}/foo`); + const doc = page.locator('.rspress-doc'); + await expect(doc).toBeVisible(); // text in string frontmatter const title = await page.$('title'); - const titleContent = await page.evaluate( - title => title?.textContent, - title, - ); + expect(await title?.textContent()).toBe('bar-title'); // replace text in shared mdx content - const h2 = await page.$('h2'); - const h2Content = await page.evaluate(h2 => h2?.textContent, h2); + const h2 = page.locator('h2'); + await expect(h2).toHaveText('#bar-h2'); // replace text in mdx content - const text = await page.$('.rspress-doc p'); - const textContent = await page.evaluate(text => text?.textContent, text); - - expect(titleContent).toEqual('bar-title'); - expect(h2Content).toEqual('#bar-h2'); - expect(textContent).toEqual('bar-content'); + const text = page.locator('.rspress-doc p'); + await expect(text).toHaveText('bar-content'); }); }); diff --git a/e2e/fixtures/search-algolia/index.test.ts b/e2e/fixtures/search-algolia/index.test.ts index a27b41f7b..6a25c5bae 100644 --- a/e2e/fixtures/search-algolia/index.test.ts +++ b/e2e/fixtures/search-algolia/index.test.ts @@ -1,10 +1,9 @@ -import assert from 'node:assert'; import { expect, test } from '@playwright/test'; import { getPort, killProcess, runDevCommand } from '../../utils/runCommands'; test.describe('search code blocks test', async () => { - let appPort; - let app; + let appPort: number; + let app: Awaited>; test.beforeAll(async () => { const appDir = __dirname; @@ -21,11 +20,10 @@ test.describe('search code blocks test', async () => { test('should search by algolia', async ({ page }) => { await page.goto(`http://localhost:${appPort}`); - const searchButton = await page.$('.DocSearch.DocSearch-Button'); - assert(searchButton); + const searchButton = page.locator('.DocSearch.DocSearch-Button'); await searchButton.click(); - const searchBar = await page.$('.DocSearch-SearchBar'); - expect(await searchBar?.isVisible()).toBeTruthy(); + const searchBar = page.locator('.DocSearch-SearchBar'); + await expect(searchBar).toBeVisible(); }); }); diff --git a/e2e/fixtures/search-code-blocks/index.test.ts b/e2e/fixtures/search-code-blocks/index.test.ts index 4a2ed23da..1fcfcef44 100644 --- a/e2e/fixtures/search-code-blocks/index.test.ts +++ b/e2e/fixtures/search-code-blocks/index.test.ts @@ -3,8 +3,8 @@ import { getPort, killProcess, runDevCommand } from '../../utils/runCommands'; import { searchInPage } from '../../utils/search'; test.describe('search code blocks test', async () => { - let appPort; - let app; + let appPort: number; + let app: Awaited>; test.beforeAll(async () => { const appDir = __dirname; diff --git a/e2e/fixtures/search-hooks/index.test.ts b/e2e/fixtures/search-hooks/index.test.ts index eb92e76e9..9981fe52b 100644 --- a/e2e/fixtures/search-hooks/index.test.ts +++ b/e2e/fixtures/search-hooks/index.test.ts @@ -18,8 +18,8 @@ function proxyConsole(page: Page) { } test.describe('search hooks', async () => { - let appPort; - let app; + let appPort: number; + let app: Awaited>; test.beforeAll(async () => { const appDir = __dirname; diff --git a/e2e/fixtures/search-i18n/index.test.ts b/e2e/fixtures/search-i18n/index.test.ts index f565026c0..9296892d8 100644 --- a/e2e/fixtures/search-i18n/index.test.ts +++ b/e2e/fixtures/search-i18n/index.test.ts @@ -3,8 +3,8 @@ import { getPort, killProcess, runDevCommand } from '../../utils/runCommands'; import { searchInPage } from '../../utils/search'; test.describe('search i18n test', async () => { - let appPort; - let app; + let appPort: number; + let app: Awaited>; test.beforeAll(async () => { const appDir = __dirname; @@ -24,21 +24,24 @@ test.describe('search i18n test', async () => { const suggestItems1 = await searchInPage(page, 'Button'); expect(await suggestItems1[0].textContent()).toContain('Button en'); - // close the search modal - await page.click('body'); + await page.keyboard.press('Escape'); + + const langMenu = page + .locator('.rp-nav__others .rp-nav-menu__item__container') + .first(); // Switch language to Chinese - await page.click('.rspress-nav-menu-group-button'); - await page.click('.rspress-nav-menu-group-content a'); + await langMenu.click(); + await page.getByRole('link', { name: '简体中文' }).click(); await page.waitForLoadState(); const suggestItems2 = await searchInPage(page, 'Button'); expect(await suggestItems2[0].textContent()).toContain('Button 中文'); - await page.click('body'); + await page.keyboard.press('Escape'); // Switch language to English - await page.click('.rspress-nav-menu-group-button'); - await page.click('.rspress-nav-menu-group-content a'); + await langMenu.click(); + await page.getByRole('link', { name: 'English' }).click(); await page.waitForLoadState(); const suggestItems3 = await searchInPage(page, 'Button'); diff --git a/e2e/fixtures/search-i18n/rspress.config.ts b/e2e/fixtures/search-i18n/rspress.config.ts index 4bc2f2012..f92ec9575 100644 --- a/e2e/fixtures/search-i18n/rspress.config.ts +++ b/e2e/fixtures/search-i18n/rspress.config.ts @@ -15,5 +15,6 @@ export default defineConfig({ label: 'English', }, ], + localeRedirect: 'never', }, }); diff --git a/e2e/fixtures/tabs-component/index.test.ts b/e2e/fixtures/tabs-component/index.test.ts index 31d67cf2a..66ff31637 100644 --- a/e2e/fixtures/tabs-component/index.test.ts +++ b/e2e/fixtures/tabs-component/index.test.ts @@ -22,32 +22,29 @@ test.describe('tabs-component test', async () => { await page.waitForSelector('.tabs-a'); // Tab A - const tabA = await page.$('.tabs-a'); - const contentA = await page.$('.tabs-a + div'); - const tabAText = await page.evaluate(node => node?.innerHTML, tabA); - const contentAText = await page.evaluate(node => node?.innerHTML, contentA); - expect(tabAText).toContain('label1'); - expect(contentAText).toContain('content1'); + const tabA = page.locator('.tabs-a'); + const contentA = page.locator('.tabs-a > div').nth(1); + await expect(tabA).toContainText('label1'); + await expect(contentA).toContainText('content1'); // Tab B - const tabB = await page.$('.tabs-b'); - const contentB = await page.$('.tabs-b + div'); - const tabBText = await page.evaluate(node => node?.innerHTML, tabB); - const contentBText = await page.evaluate(node => node?.innerHTML, contentB); - expect(tabBText).toContain('label2'); - expect(contentBText).toContain('content2'); - const notSelected = await page.$$eval( - '.tabs-b div', - divs => divs.filter(div => div.className.includes('not-selected')).length, - ); + const tabB = page.locator('.tabs-b'); + const contentB = page.locator('.tabs-b > div').nth(1); + await expect(tabB).toContainText('label2'); + await expect(contentB).toContainText('content2'); + const notSelected = await page + .locator('.tabs-b div') + .filter({ hasText: /./ }) + .evaluateAll( + divs => + divs.filter(div => div.className.includes('not-selected')).length, + ); expect(notSelected).toEqual(2); // Tab C - const tabC = await page.$('.tabs-c'); - const contentC = await page.$('.tabs-c + div'); - const tabCText = await page.evaluate(node => node?.innerHTML, tabC); - const contentCText = await page.evaluate(node => node?.innerHTML, contentC); - expect(tabCText).toEqual(''); - expect(contentCText).toEqual(''); + const tabC = page.locator('.tabs-c'); + const contentC = page.locator('.tabs-c > div'); + await expect(tabC).toHaveText(''); + await expect(contentC).toHaveText(''); }); }); diff --git a/e2e/fixtures/theme-css/index.test.ts b/e2e/fixtures/theme-css/index.test.ts index 6f3b19291..6e883d22d 100644 --- a/e2e/fixtures/theme-css/index.test.ts +++ b/e2e/fixtures/theme-css/index.test.ts @@ -18,10 +18,7 @@ test.describe('theme-css-order', async () => { test('globalStyles should work', async ({ page }) => { await page.goto(`http://localhost:${appPort}`); - const link = await page.$('.rspress-doc a'); - const colorValue = await link?.evaluate( - element => getComputedStyle(element).color, - ); - expect(colorValue).toEqual('rgb(255, 165, 0)'); + const link = page.locator('.rspress-doc a:not(.rp-header-anchor)'); + await expect(link).toHaveCSS('color', 'rgb(255, 165, 0)'); }); }); diff --git a/e2e/fixtures/title-number/index.test.ts b/e2e/fixtures/title-number/index.test.ts index bb44ab9de..7b63478aa 100644 --- a/e2e/fixtures/title-number/index.test.ts +++ b/e2e/fixtures/title-number/index.test.ts @@ -18,17 +18,19 @@ test.describe('title-number test', async () => { test('Index page', async ({ page }) => { await page.goto(`http://localhost:${appPort}`); - const h3 = await page.$$('h3'); - const textList = await page.evaluate(h3 => h3.map(h => h.textContent), h3); + const h3Elements = page.locator('h3'); + await expect(h3Elements).toHaveText([ + '#-22222222', + '#-1111111111', + '#-1111111111', + ]); - expect(textList).toEqual(['#-22222222', '#-1111111111', '#-1111111111']); - - const h3A = await page.$$('h3 a'); - const hrefList = await page.evaluate( - h3A => h3A.map(h => h?.getAttribute('href')), - h3A, + const h3Links = page.locator('h3 a'); + const hrefList = await Promise.all( + Array.from({ length: await h3Links.count() }, async (_, i) => + h3Links.nth(i).getAttribute('href'), + ), ); - expect(hrefList).toEqual(['#-22222222', '#-1111111111', '#-1111111111-1']); }); }); diff --git a/e2e/fixtures/view-transition/index.test.ts b/e2e/fixtures/view-transition/index.test.ts index 667aa783b..0f29d1ebc 100644 --- a/e2e/fixtures/view-transition/index.test.ts +++ b/e2e/fixtures/view-transition/index.test.ts @@ -35,7 +35,7 @@ test.describe('basic test', async () => { expect(payload.animation.type).toMatch(/[CSSTransition|CSSAnimation]/); resolve(); }); - const alinks = await page.locator('.rspress-nav-menu a').all(); + const alinks = await page.locator('.rp-nav-menu a').all(); const secondLink = alinks[1]; secondLink.click(); await page.waitForURL('**/start**'); diff --git a/e2e/fixtures/with-base/index.test.ts b/e2e/fixtures/with-base/index.test.ts index ebce56e4d..2252bf09a 100644 --- a/e2e/fixtures/with-base/index.test.ts +++ b/e2e/fixtures/with-base/index.test.ts @@ -27,10 +27,8 @@ test.describe('plugin test', async () => { waitUntil: 'networkidle', }); // take the sidebar - const sidebar = await page.$$( - '.rspress-sidebar .rspress-scrollbar > nav > section', - ); - expect(sidebar?.length).toBe(1); + const sidebar = page.locator('.rp-doc-layout__sidebar'); + await expect(sidebar).toHaveCount(1); // get the section }); @@ -38,9 +36,9 @@ test.describe('plugin test', async () => { await page.goto(`http://localhost:${appPort}/base/en/guide/quick-start`, { waitUntil: 'networkidle', }); - const a = await page.$('.rspress-doc a:not(.header-anchor)'); + const a = page.locator('.rspress-doc a:not(.rp-header-anchor)'); // extract the href of a tag - const href = await page.evaluate(a => a?.getAttribute('href'), a); + const href = await a.getAttribute('href'); expect(href).toBe('/base/en/guide/install.html'); }); @@ -48,17 +46,15 @@ test.describe('plugin test', async () => { await page.goto(`http://localhost:${appPort}/base`, { waitUntil: 'networkidle', }); - const docContent = await page.$('.rspress-doc'); - const text = await docContent?.textContent(); - expect(text?.includes('This is the index page')).toBeTruthy(); + const docContent = page.locator('.rspress-doc'); + await expect(docContent).toContainText('This is the index page'); }); test('Should render the homepage - "/base/"', async ({ page }) => { await page.goto(`http://localhost:${appPort}/base/`, { waitUntil: 'networkidle', }); - const docContent = await page.$('.rspress-doc'); - const text = await docContent?.textContent(); - expect(text?.includes('This is the index page')).toBeTruthy(); + const docContent = page.locator('.rspress-doc'); + await expect(docContent).toContainText('This is the index page'); }); }); diff --git a/e2e/utils/getSideBar.ts b/e2e/utils/getSideBar.ts index 66b85a4d7..05e25b66a 100644 --- a/e2e/utils/getSideBar.ts +++ b/e2e/utils/getSideBar.ts @@ -1,30 +1,24 @@ -import type { Page } from '@playwright/test'; +import type { Locator, Page } from '@playwright/test'; -export async function getNavbar(page: Page) { - // take the nav - const nav = await page.$$('.rspress-nav-menu'); - return nav; +export function getNavbar(page: Page): Locator { + // Query both nav menus (left + right) rendered in the layout + return page.locator('.rp-nav-menu'); } -export async function getNavbarItems(page: Page) { - // take the nav - const nav = await page.$$('.rspress-nav-menu .rspress-nav-menu-item'); - return nav; +export function getNavbarItems(page: Page): Locator { + // Query actual nav menu items + return page.locator('.rp-nav-menu .rp-nav-menu__item'); } -export async function getSidebar(page: Page) { - // take the sidebar, properly a section or a tag - const sidebar = await page.$$( - `.rspress-sidebar .rspress-scrollbar > nav > section, - .rspress-sidebar .rspress-scrollbar > nav > div.rspress-sidebar-item > a`, - ); - return sidebar; +export function getSidebar(page: Page): Locator { + // Query all sidebar items including nested entries + return page.locator('.rp-doc-layout__sidebar .rp-sidebar-item'); } -export async function getSidebarTexts(page: Page) { - const sidebar = await getSidebar(page); - const sidebarTexts = await Promise.all( - sidebar.map(element => element.textContent()), - ); - return sidebarTexts; +export async function getSidebarTexts(page: Page): Promise { + const sidebar = getSidebar(page); + const texts = await sidebar.allTextContents(); + return texts + .map(text => text.trim()) + .filter((text): text is string => text.length > 0); } diff --git a/e2e/utils/search.ts b/e2e/utils/search.ts index fff045433..c24e6484f 100644 --- a/e2e/utils/search.ts +++ b/e2e/utils/search.ts @@ -2,8 +2,11 @@ import assert from 'node:assert'; import type { Page } from '@playwright/test'; async function getSearchButton(page: Page) { - const searchButton = await page.$('.rp-flex > .rspress-nav-search-button'); - return searchButton; + const desktopButton = await page.$('.rp-search-button'); + if (desktopButton) { + return desktopButton; + } + return page.$('.rp-search-button--mobile'); } /** @@ -14,25 +17,23 @@ export async function searchInPage( searchText: string, reset = true, ) { - const searchInputLoc = page.locator('.rspress-search-panel-input'); + const searchInputLoc = page.locator('.rp-search-panel__input'); const isSearchInputVisible = await searchInputLoc.isVisible(); if (!isSearchInputVisible) { const searchButton = await getSearchButton(page); assert(searchButton); await searchButton.click(); - const searchInput = await page.$('.rspress-search-panel-input'); - assert(searchInput); + await page.waitForSelector('.rp-search-panel__input'); } - const searchInput = await page.$('.rspress-search-panel-input'); + const searchInput = await page.$('.rp-search-panel__input'); assert(searchInput); const isEditable = await searchInput.isEditable(); assert(isEditable); await searchInput.focus(); await page.keyboard.type(searchText); await page.waitForTimeout(400); - const elements = await page.$$('.rspress-search-suggest-item'); + const elements = await page.$$('.rp-suggest-item'); - // reset if (reset) { for (let i = 0; i < searchText.length; i++) { await page.keyboard.press('Backspace'); diff --git a/packages/core/index.html b/packages/core/index.html index c8d48707a..a6cdc1b08 100644 --- a/packages/core/index.html +++ b/packages/core/index.html @@ -9,7 +9,7 @@ -
-
+
+
diff --git a/packages/core/src/node/auto-nav-sidebar/normalize.ts b/packages/core/src/node/auto-nav-sidebar/normalize.ts index 7fcf6bbe0..87edd72bb 100644 --- a/packages/core/src/node/auto-nav-sidebar/normalize.ts +++ b/packages/core/src/node/auto-nav-sidebar/normalize.ts @@ -33,6 +33,7 @@ function getFileKey(realPath: string | undefined, docsDir: string) { async function fsDirToMetaItems( workDir: string, + docsDir: string, extensions: string[], ): Promise { let subItems: string[]; @@ -66,6 +67,10 @@ async function fsDirToMetaItems( const stat = await fsStat(join(workDir, item)); // If the item is a directory, we will transform it to a object with `type` and `name` property. if (stat.isDirectory()) { + // ignore public folder + if (item === 'public' && workDir === docsDir) { + return null; + } return { type: 'dir', name: item, @@ -247,7 +252,7 @@ async function metaDirItemToSidebarItem( metaFileSet.add(dirMetaJsonPath); dirMetaJson = await readJson(dirMetaJsonPath); } else { - dirMetaJson = await fsDirToMetaItems(dirAbsolutePath, extensions); + dirMetaJson = await fsDirToMetaItems(dirAbsolutePath, docsDir, extensions); } async function getItems( diff --git a/packages/core/src/node/mdx/rehypePlugins/__snapshots__/headerAnchor.test.ts.snap b/packages/core/src/node/mdx/rehypePlugins/__snapshots__/headerAnchor.test.ts.snap index cfbde7e4a..c59786eee 100644 --- a/packages/core/src/node/mdx/rehypePlugins/__snapshots__/headerAnchor.test.ts.snap +++ b/packages/core/src/node/mdx/rehypePlugins/__snapshots__/headerAnchor.test.ts.snap @@ -5,6 +5,7 @@ exports[`rehypeHeadAnchor > basic 1`] = ` /*prettier-ignore-end*/ import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime"; import {useMDXComponents as _provideComponents} from "@mdx-js/react"; +import {Callout as $$$callout$$$} from "@theme"; function _createMdxContent(props) { const _components = { a: "a", @@ -20,7 +21,7 @@ function _createMdxContent(props) { children: [_jsxs(_components.h1, { id: "guide", children: [_jsx(_components.a, { - className: "header-anchor", + className: "rp-header-anchor", "aria-hidden": "true", href: "#guide", children: "#" @@ -28,7 +29,7 @@ function _createMdxContent(props) { }), "\\n", _jsxs(_components.h2, { id: "custom-id", children: [_jsx(_components.a, { - className: "header-anchor", + className: "rp-header-anchor", "aria-hidden": "true", href: "#custom-id", children: "#" @@ -36,7 +37,7 @@ function _createMdxContent(props) { }), "\\n", _jsxs(_components.h2, { id: "custom-id", children: [_jsx(_components.a, { - className: "header-anchor", + className: "rp-header-anchor", "aria-hidden": "true", href: "#custom-id", children: "#" @@ -44,7 +45,7 @@ function _createMdxContent(props) { }), "\\n", _jsxs(_components.h2, { id: "title-2", children: [_jsx(_components.a, { - className: "header-anchor", + className: "rp-header-anchor", "aria-hidden": "true", href: "#title-2", children: "#" @@ -52,7 +53,7 @@ function _createMdxContent(props) { }), "\\n", _jsxs(_components.h2, { id: "title-2-1", children: [_jsx(_components.a, { - className: "header-anchor", + className: "rp-header-anchor", "aria-hidden": "true", href: "#title-2-1", children: "#" @@ -60,7 +61,7 @@ function _createMdxContent(props) { }), "\\n", _jsxs(_components.h2, { id: "title-2-2", children: [_jsx(_components.a, { - className: "header-anchor", + className: "rp-header-anchor", "aria-hidden": "true", href: "#title-2-2", children: "#" @@ -70,7 +71,7 @@ function _createMdxContent(props) { }), "\\n", _jsxs(_components.h2, { id: "title-2-3", children: [_jsx(_components.a, { - className: "header-anchor", + className: "rp-header-anchor", "aria-hidden": "true", href: "#title-2-3", children: "#" @@ -78,7 +79,7 @@ function _createMdxContent(props) { }), "\\n", _jsxs(_components.h2, { id: "title-2-4", children: [_jsx(_components.a, { - className: "header-anchor", + className: "rp-header-anchor", "aria-hidden": "true", href: "#title-2-4", children: "#" @@ -88,7 +89,7 @@ function _createMdxContent(props) { }), "\\n", "\\n", _jsxs(_components.h2, { id: "title-2-5", children: [_jsx(_components.a, { - className: "header-anchor", + className: "rp-header-anchor", "aria-hidden": "true", href: "#title-2-5", children: "#" @@ -120,6 +121,7 @@ MDXContent.__RSPRESS_PAGE_META["index.mdx"] = {"toc":[{"id":"custom-id","text":" exports[`rehypeHeadAnchor > should render inline code in title 1`] = ` "import {jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime"; import {useMDXComponents as _provideComponents} from "@mdx-js/react"; +import {Callout as $$$callout$$$} from "@theme"; function _createMdxContent(props) { const _components = { a: "a", @@ -131,7 +133,7 @@ function _createMdxContent(props) { return _jsxs(_components.h1, { id: "hello-world-inline-code", children: [_jsx(_components.a, { - className: "header-anchor", + className: "rp-header-anchor", "aria-hidden": "true", href: "#hello-world-inline-code", children: "#" @@ -162,6 +164,7 @@ MDXContent.__RSPRESS_PAGE_META["inline-code.mdx"] = {"toc":[],"title":"Hello Wor exports[`rehypeHeadAnchor > should support custom id 1`] = ` "import {jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime"; import {useMDXComponents as _provideComponents} from "@mdx-js/react"; +import {Callout as $$$callout$$$} from "@theme"; function _createMdxContent(props) { const _components = { a: "a", @@ -173,7 +176,7 @@ function _createMdxContent(props) { return _jsxs(_components.h1, { id: "custom-id", children: [_jsx(_components.a, { - className: "header-anchor", + className: "rp-header-anchor", "aria-hidden": "true", href: "#custom-id", children: "#" @@ -204,6 +207,7 @@ MDXContent.__RSPRESS_PAGE_META["inline-code.mdx"] = {"toc":[],"title":"Hello Wor exports[`rehypeHeadAnchor > should support mdx component with trim 1`] = ` "import {jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime"; import {useMDXComponents as _provideComponents} from "@mdx-js/react"; +import {Callout as $$$callout$$$} from "@theme"; function _createMdxContent(props) { const _components = { a: "a", @@ -215,7 +219,7 @@ function _createMdxContent(props) { return _jsxs(_components.h1, { id: "hello-world", children: [_jsx(_components.a, { - className: "header-anchor", + className: "rp-header-anchor", "aria-hidden": "true", href: "#hello-world", children: "#" diff --git a/packages/core/src/node/mdx/rehypePlugins/headerAnchor.ts b/packages/core/src/node/mdx/rehypePlugins/headerAnchor.ts index d5ec190cb..a25ee9fe0 100644 --- a/packages/core/src/node/mdx/rehypePlugins/headerAnchor.ts +++ b/packages/core/src/node/mdx/rehypePlugins/headerAnchor.ts @@ -62,7 +62,7 @@ function create(node: Element): Element { type: 'element', tagName: 'a', properties: { - class: 'header-anchor', + class: 'rp-header-anchor', ariaHidden: 'true', href: `#${node.properties!.id}`, }, diff --git a/packages/core/src/node/mdx/remarkPlugins/__snapshots__/containerSyntax.test.ts.snap b/packages/core/src/node/mdx/remarkPlugins/__snapshots__/containerSyntax.test.ts.snap index 3b3f0dcde..29dbc0f94 100644 --- a/packages/core/src/node/mdx/remarkPlugins/__snapshots__/containerSyntax.test.ts.snap +++ b/packages/core/src/node/mdx/remarkPlugins/__snapshots__/containerSyntax.test.ts.snap @@ -1,26 +1,21 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`remark-container > Has space before ::: 1`] = ` -"import {jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime"; +"import {jsx as _jsx} from "react/jsx-runtime"; import {useMDXComponents as _provideComponents} from "@mdx-js/react"; +import {Callout as $$$callout$$$} from "@theme"; function _createMdxContent(props) { const _components = { - div: "div", p: "p", ..._provideComponents(), ...props.components }; - return _jsxs(_components.div, { - className: "rspress-directive tip", - children: [_jsx(_components.div, { - className: "rspress-directive-title", - children: "title" - }), _jsx(_components.div, { - className: "rspress-directive-content", - children: _jsx(_components.p, { - children: "\\nsss" - }) - })] + return _jsx($$$callout$$$, { + type: "tip", + title: "title", + children: _jsx(_components.p, { + children: "\\nsss" + }) }); } export default function MDXContent(props = {}) { @@ -43,26 +38,21 @@ MDXContent.__RSPRESS_PAGE_META["index.mdx"] = {"toc":[],"title":"","headingTitle `; exports[`remark-container > Has space before type 1`] = ` -"import {jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime"; +"import {jsx as _jsx} from "react/jsx-runtime"; import {useMDXComponents as _provideComponents} from "@mdx-js/react"; +import {Callout as $$$callout$$$} from "@theme"; function _createMdxContent(props) { const _components = { - div: "div", p: "p", ..._provideComponents(), ...props.components }; - return _jsxs(_components.div, { - className: "rspress-directive tip", - children: [_jsx(_components.div, { - className: "rspress-directive-title", - children: "TIP" - }), _jsx(_components.div, { - className: "rspress-directive-content", - children: _jsx(_components.p, { - children: "This is a tip." - }) - })] + return _jsx($$$callout$$$, { + type: "tip", + title: "Tip", + children: _jsx(_components.p, { + children: "This is a tip." + }) }); } export default function MDXContent(props = {}) { @@ -87,25 +77,20 @@ MDXContent.__RSPRESS_PAGE_META["index.mdx"] = {"toc":[],"title":"","headingTitle exports[`remark-container > No newline 1`] = ` "import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime"; import {useMDXComponents as _provideComponents} from "@mdx-js/react"; +import {Callout as $$$callout$$$} from "@theme"; function _createMdxContent(props) { const _components = { - div: "div", p: "p", ..._provideComponents(), ...props.components }; return _jsxs(_Fragment, { - children: [_jsxs(_components.div, { - className: "rspress-directive tip", - children: [_jsx(_components.div, { - className: "rspress-directive-title", - children: "TIP" - }), _jsx(_components.div, { - className: "rspress-directive-content", - children: _jsx(_components.p, { - children: "\\nThis is a tip" - }) - })] + children: [_jsx($$$callout$$$, { + type: "tip", + title: "Tip", + children: _jsx(_components.p, { + children: "\\nThis is a tip" + }) }), "\\n", _jsx(_components.p, { children: "12312" })] @@ -131,26 +116,21 @@ MDXContent.__RSPRESS_PAGE_META["index.mdx"] = {"toc":[],"title":"","headingTitle `; exports[`remark-container > Use \\{title="foo"} as title 1`] = ` -"import {jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime"; +"import {jsx as _jsx} from "react/jsx-runtime"; import {useMDXComponents as _provideComponents} from "@mdx-js/react"; +import {Callout as $$$callout$$$} from "@theme"; function _createMdxContent(props) { const _components = { - div: "div", p: "p", ..._provideComponents(), ...props.components }; - return _jsxs(_components.div, { - className: "rspress-directive tip", - children: [_jsx(_components.div, { - className: "rspress-directive-title", - children: "Custom title" - }), _jsx(_components.div, { - className: "rspress-directive-content", - children: _jsx(_components.p, { - children: "This is a tip." - }) - })] + return _jsx($$$callout$$$, { + type: "tip", + title: "Custom title", + children: _jsx(_components.p, { + children: "This is a tip." + }) }); } export default function MDXContent(props = {}) { @@ -173,26 +153,21 @@ MDXContent.__RSPRESS_PAGE_META["index.mdx"] = {"toc":[],"title":"","headingTitle `; exports[`remark-container > Use \\{title='foo'} as title 1`] = ` -"import {jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime"; +"import {jsx as _jsx} from "react/jsx-runtime"; import {useMDXComponents as _provideComponents} from "@mdx-js/react"; +import {Callout as $$$callout$$$} from "@theme"; function _createMdxContent(props) { const _components = { - div: "div", p: "p", ..._provideComponents(), ...props.components }; - return _jsxs(_components.div, { - className: "rspress-directive tip", - children: [_jsx(_components.div, { - className: "rspress-directive-title", - children: "Custom title" - }), _jsx(_components.div, { - className: "rspress-directive-content", - children: _jsx(_components.p, { - children: "This is a tip." - }) - })] + return _jsx($$$callout$$$, { + type: "tip", + title: "Custom title", + children: _jsx(_components.p, { + children: "This is a tip." + }) }); } export default function MDXContent(props = {}) { @@ -215,26 +190,21 @@ MDXContent.__RSPRESS_PAGE_META["index.mdx"] = {"toc":[],"title":"","headingTitle `; exports[`remark-container > Use \\{title=foo} as title 1`] = ` -"import {jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime"; +"import {jsx as _jsx} from "react/jsx-runtime"; import {useMDXComponents as _provideComponents} from "@mdx-js/react"; +import {Callout as $$$callout$$$} from "@theme"; function _createMdxContent(props) { const _components = { - div: "div", p: "p", ..._provideComponents(), ...props.components }; - return _jsxs(_components.div, { - className: "rspress-directive tip", - children: [_jsx(_components.div, { - className: "rspress-directive-title", - children: "Custom title" - }), _jsx(_components.div, { - className: "rspress-directive-content", - children: _jsx(_components.p, { - children: "This is a tip." - }) - })] + return _jsx($$$callout$$$, { + type: "tip", + title: "Custom title", + children: _jsx(_components.p, { + children: "This is a tip." + }) }); } export default function MDXContent(props = {}) { @@ -257,26 +227,21 @@ MDXContent.__RSPRESS_PAGE_META["index.mdx"] = {"toc":[],"title":"","headingTitle `; exports[`remark-container > With a new line after the start position of container 1`] = ` -"import {jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime"; +"import {jsx as _jsx} from "react/jsx-runtime"; import {useMDXComponents as _provideComponents} from "@mdx-js/react"; +import {Callout as $$$callout$$$} from "@theme"; function _createMdxContent(props) { const _components = { - div: "div", p: "p", ..._provideComponents(), ...props.components }; - return _jsxs(_components.div, { - className: "rspress-directive tip", - children: [_jsx(_components.div, { - className: "rspress-directive-title", - children: "TIP" - }), _jsx(_components.div, { - className: "rspress-directive-content", - children: _jsx(_components.p, { - children: "This is a tip" - }) - })] + return _jsx($$$callout$$$, { + type: "tip", + title: "Tip", + children: _jsx(_components.p, { + children: "This is a tip" + }) }); } export default function MDXContent(props = {}) { @@ -301,35 +266,30 @@ MDXContent.__RSPRESS_PAGE_META["index.mdx"] = {"toc":[],"title":"","headingTitle exports[`remark-container > With block quote in container 1`] = ` "import {jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime"; import {useMDXComponents as _provideComponents} from "@mdx-js/react"; +import {Callout as $$$callout$$$} from "@theme"; function _createMdxContent(props) { const _components = { a: "a", blockquote: "blockquote", code: "code", - div: "div", p: "p", ..._provideComponents(), ...props.components }; - return _jsxs(_components.div, { - className: "rspress-directive tip", - children: [_jsx(_components.div, { - className: "rspress-directive-title", - children: "TIP" - }), _jsxs(_components.div, { - className: "rspress-directive-content", - children: [_jsxs(_components.p, { - children: ["This is a tip with ", _jsx(_components.code, { - children: "code" - }), " and ", _jsx(_components.a, { - href: "foo", - children: "link" - }), " some text"] - }), _jsxs(_components.blockquote, { - children: ["\\n", _jsx(_components.p, { - children: "This is a quote" - }), "\\n"] - })] + return _jsxs($$$callout$$$, { + type: "tip", + title: "Tip", + children: [_jsxs(_components.p, { + children: ["This is a tip with ", _jsx(_components.code, { + children: "code" + }), " and ", _jsx(_components.a, { + href: "foo", + children: "link" + }), " some text"] + }), _jsxs(_components.blockquote, { + children: ["\\n", _jsx(_components.p, { + children: "This is a quote" + }), "\\n"] })] }); } @@ -355,73 +315,68 @@ MDXContent.__RSPRESS_PAGE_META["index.mdx"] = {"toc":[],"title":"","headingTitle exports[`remark-container > With code block in container 1`] = ` "import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime"; import {useMDXComponents as _provideComponents} from "@mdx-js/react"; +import {Callout as $$$callout$$$} from "@theme"; function _createMdxContent(props) { const _components = { a: "a", code: "code", - div: "div", p: "p", pre: "pre", span: "span", ..._provideComponents(), ...props.components }; - return _jsxs(_components.div, { - className: "rspress-directive tip", - children: [_jsx(_components.div, { - className: "rspress-directive-title", - children: "TIP" - }), _jsxs(_components.div, { - className: "rspress-directive-content", - children: [_jsxs(_components.p, { - children: ["This is a tip with ", _jsx(_components.code, { - children: "code" - }), " and ", _jsx(_components.a, { - href: "foo", - children: "link" - }), " some text"] - }), _jsx(_Fragment, { - children: _jsx(_components.pre, { - className: "shiki css-variables", - style: { - backgroundColor: "var(--shiki-background)", - color: "var(--shiki-foreground)" - }, - tabIndex: "0", - children: _jsx(_components.code, { - className: "language-js", - children: _jsxs(_components.span, { - className: "line", - children: [_jsx(_components.span, { - style: { - color: "var(--shiki-token-keyword)" - }, - children: "const" - }), _jsx(_components.span, { - style: { - color: "var(--shiki-token-constant)" - }, - children: " a" - }), _jsx(_components.span, { - style: { - color: "var(--shiki-token-keyword)" - }, - children: " =" - }), _jsx(_components.span, { - style: { - color: "var(--shiki-token-constant)" - }, - children: " 1" - }), _jsx(_components.span, { - style: { - color: "var(--shiki-foreground)" - }, - children: ";" - })] - }) + return _jsxs($$$callout$$$, { + type: "tip", + title: "Tip", + children: [_jsxs(_components.p, { + children: ["This is a tip with ", _jsx(_components.code, { + children: "code" + }), " and ", _jsx(_components.a, { + href: "foo", + children: "link" + }), " some text"] + }), _jsx(_Fragment, { + children: _jsx(_components.pre, { + className: "shiki css-variables", + style: { + backgroundColor: "var(--shiki-background)", + color: "var(--shiki-foreground)" + }, + tabIndex: "0", + children: _jsx(_components.code, { + className: "language-js", + children: _jsxs(_components.span, { + className: "line", + children: [_jsx(_components.span, { + style: { + color: "var(--shiki-token-keyword)" + }, + children: "const" + }), _jsx(_components.span, { + style: { + color: "var(--shiki-token-constant)" + }, + children: " a" + }), _jsx(_components.span, { + style: { + color: "var(--shiki-token-keyword)" + }, + children: " =" + }), _jsx(_components.span, { + style: { + color: "var(--shiki-token-constant)" + }, + children: " 1" + }), _jsx(_components.span, { + style: { + color: "var(--shiki-foreground)" + }, + children: ";" + })] }) }) - })] + }) })] }); } @@ -448,11 +403,11 @@ exports[`remark-container > With link, inlineCode, img, list in container 1`] = "import {jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime"; import {useMDXComponents as _provideComponents} from "@mdx-js/react"; import image0 from "foo"; +import {Callout as $$$callout$$$} from "@theme"; function _createMdxContent(props) { const _components = { a: "a", code: "code", - div: "div", img: "img", li: "li", p: "p", @@ -460,32 +415,27 @@ function _createMdxContent(props) { ..._provideComponents(), ...props.components }; - return _jsxs(_components.div, { - className: "rspress-directive tip", - children: [_jsx(_components.div, { - className: "rspress-directive-title", - children: "TIP" - }), _jsxs(_components.div, { - className: "rspress-directive-content", - children: [_jsxs(_components.p, { - children: ["This is a tip with ", _jsx(_components.code, { - children: "code" - }), " and ", _jsx(_components.a, { - href: "foo", - children: "link" - }), " some text"] - }), _jsxs(_components.ul, { - children: ["\\n", _jsx(_components.li, { - children: "list 1" - }), "\\n", _jsx(_components.li, { - children: "list 2" - }), "\\n"] - }), _jsx(_components.p, { - children: _jsx(_components.img, { - alt: "img", - src: image0 - }) - })] + return _jsxs($$$callout$$$, { + type: "tip", + title: "Tip", + children: [_jsxs(_components.p, { + children: ["This is a tip with ", _jsx(_components.code, { + children: "code" + }), " and ", _jsx(_components.a, { + href: "foo", + children: "link" + }), " some text"] + }), _jsxs(_components.ul, { + children: ["\\n", _jsx(_components.li, { + children: "list 1" + }), "\\n", _jsx(_components.li, { + children: "list 2" + }), "\\n"] + }), _jsx(_components.p, { + children: _jsx(_components.img, { + alt: "img", + src: image0 + }) })] }); } @@ -512,11 +462,11 @@ exports[`remark-container > With link, inlineCode, img, spread list in container "import {jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime"; import {useMDXComponents as _provideComponents} from "@mdx-js/react"; import image0 from "foo"; +import {Callout as $$$callout$$$} from "@theme"; function _createMdxContent(props) { const _components = { a: "a", code: "code", - div: "div", img: "img", li: "li", p: "p", @@ -524,32 +474,27 @@ function _createMdxContent(props) { ..._provideComponents(), ...props.components }; - return _jsxs(_components.div, { - className: "rspress-directive tip", - children: [_jsx(_components.div, { - className: "rspress-directive-title", - children: "TIP" - }), _jsxs(_components.div, { - className: "rspress-directive-content", - children: [_jsxs(_components.p, { - children: ["This is a tip with ", _jsx(_components.code, { - children: "code" - }), " and ", _jsx(_components.a, { - href: "foo", - children: "link" - }), " some text"] - }), _jsxs(_components.ul, { - children: ["\\n", _jsx(_components.li, { - children: "list 1" - }), "\\n", _jsx(_components.li, { - children: "list 2" - }), "\\n"] - }), _jsx(_components.p, { - children: _jsx(_components.img, { - alt: "img", - src: image0 - }) - })] + return _jsxs($$$callout$$$, { + type: "tip", + title: "Tip", + children: [_jsxs(_components.p, { + children: ["This is a tip with ", _jsx(_components.code, { + children: "code" + }), " and ", _jsx(_components.a, { + href: "foo", + children: "link" + }), " some text"] + }), _jsxs(_components.ul, { + children: ["\\n", _jsx(_components.li, { + children: "list 1" + }), "\\n", _jsx(_components.li, { + children: "list 2" + }), "\\n"] + }), _jsx(_components.p, { + children: _jsx(_components.img, { + alt: "img", + src: image0 + }) })] }); } @@ -573,26 +518,21 @@ MDXContent.__RSPRESS_PAGE_META["index.mdx"] = {"toc":[],"title":"","headingTitle `; exports[`remark-container > With new line before the end position of container 1`] = ` -"import {jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime"; +"import {jsx as _jsx} from "react/jsx-runtime"; import {useMDXComponents as _provideComponents} from "@mdx-js/react"; +import {Callout as $$$callout$$$} from "@theme"; function _createMdxContent(props) { const _components = { - div: "div", p: "p", ..._provideComponents(), ...props.components }; - return _jsxs(_components.div, { - className: "rspress-directive tip", - children: [_jsx(_components.div, { - className: "rspress-directive-title", - children: "TIP" - }), _jsx(_components.div, { - className: "rspress-directive-content", - children: _jsx(_components.p, { - children: "\\nThis is a tip" - }) - })] + return _jsx($$$callout$$$, { + type: "tip", + title: "Tip", + children: _jsx(_components.p, { + children: "\\nThis is a tip" + }) }); } export default function MDXContent(props = {}) { @@ -615,26 +555,21 @@ MDXContent.__RSPRESS_PAGE_META["index.mdx"] = {"toc":[],"title":"","headingTitle `; exports[`remark-container > With new line in all case 1`] = ` -"import {jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime"; +"import {jsx as _jsx} from "react/jsx-runtime"; import {useMDXComponents as _provideComponents} from "@mdx-js/react"; +import {Callout as $$$callout$$$} from "@theme"; function _createMdxContent(props) { const _components = { - div: "div", p: "p", ..._provideComponents(), ...props.components }; - return _jsxs(_components.div, { - className: "rspress-directive tip", - children: [_jsx(_components.div, { - className: "rspress-directive-title", - children: "TIP" - }), _jsx(_components.div, { - className: "rspress-directive-content", - children: _jsx(_components.p, { - children: "This is a tip" - }) - })] + return _jsx($$$callout$$$, { + type: "tip", + title: "Tip", + children: _jsx(_components.p, { + children: "This is a tip" + }) }); } export default function MDXContent(props = {}) { @@ -657,28 +592,21 @@ MDXContent.__RSPRESS_PAGE_META["index.mdx"] = {"toc":[],"title":"","headingTitle `; exports[`remark-container > details 1`] = ` -"import {jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime"; +"import {jsx as _jsx} from "react/jsx-runtime"; import {useMDXComponents as _provideComponents} from "@mdx-js/react"; +import {Callout as $$$callout$$$} from "@theme"; function _createMdxContent(props) { const _components = { - details: "details", - div: "div", p: "p", - summary: "summary", ..._provideComponents(), ...props.components }; - return _jsxs(_components.details, { - className: "rspress-directive details", - children: [_jsx(_components.summary, { - className: "rspress-directive-title", - children: "DETAILS" - }), _jsx(_components.div, { - className: "rspress-directive-content", - children: _jsx(_components.p, { - children: "\\nThis is a details block." - }) - })] + return _jsx($$$callout$$$, { + type: "details", + title: "Details", + children: _jsx(_components.p, { + children: "\\nThis is a details block." + }) }); } export default function MDXContent(props = {}) { @@ -703,6 +631,7 @@ MDXContent.__RSPRESS_PAGE_META["index.mdx"] = {"toc":[],"title":"","headingTitle exports[`remark-container > empty blockquote 1`] = ` "import {jsx as _jsx} from "react/jsx-runtime"; import {useMDXComponents as _provideComponents} from "@mdx-js/react"; +import {Callout as $$$callout$$$} from "@theme"; function _createMdxContent(props) { const _components = { blockquote: "blockquote", @@ -735,31 +664,26 @@ MDXContent.__RSPRESS_PAGE_META["index.mdx"] = {"toc":[],"title":"","headingTitle exports[`remark-container > end with a link 1`] = ` "import {jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime"; import {useMDXComponents as _provideComponents} from "@mdx-js/react"; +import {Callout as $$$callout$$$} from "@theme"; function _createMdxContent(props) { const _components = { a: "a", - div: "div", p: "p", ..._provideComponents(), ...props.components }; - return _jsxs(_components.div, { - className: "rspress-directive tip", - children: [_jsx(_components.div, { - className: "rspress-directive-title", - children: "TIP" - }), _jsxs(_components.div, { - className: "rspress-directive-content", - children: [_jsx(_components.p, { - children: "\\nLine 1." - }), _jsxs(_components.p, { - children: ["Line 2 with ", _jsx(_components.a, { - href: "http://example.com", - rel: "noopener noreferrer", - target: "_blank", - children: "link" - }), "."] - })] + return _jsxs($$$callout$$$, { + type: "tip", + title: "Tip", + children: [_jsx(_components.p, { + children: "\\nLine 1." + }), _jsxs(_components.p, { + children: ["Line 2 with ", _jsx(_components.a, { + href: "http://example.com", + rel: "noopener noreferrer", + target: "_blank", + children: "link" + }), "."] })] }); } @@ -785,31 +709,26 @@ MDXContent.__RSPRESS_PAGE_META["index.mdx"] = {"toc":[],"title":"","headingTitle exports[`remark-container > end with a new line 1`] = ` "import {jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime"; import {useMDXComponents as _provideComponents} from "@mdx-js/react"; +import {Callout as $$$callout$$$} from "@theme"; function _createMdxContent(props) { const _components = { a: "a", - div: "div", p: "p", ..._provideComponents(), ...props.components }; - return _jsxs(_components.div, { - className: "rspress-directive tip", - children: [_jsx(_components.div, { - className: "rspress-directive-title", - children: "TIP" - }), _jsxs(_components.div, { - className: "rspress-directive-content", - children: [_jsx(_components.p, { - children: "\\nLine 1." - }), _jsxs(_components.p, { - children: ["Line 2 with ", _jsx(_components.a, { - href: "http://example.com", - rel: "noopener noreferrer", - target: "_blank", - children: "link" - }), "."] - })] + return _jsxs($$$callout$$$, { + type: "tip", + title: "Tip", + children: [_jsx(_components.p, { + children: "\\nLine 1." + }), _jsxs(_components.p, { + children: ["Line 2 with ", _jsx(_components.a, { + href: "http://example.com", + rel: "noopener noreferrer", + target: "_blank", + children: "link" + }), "."] })] }); } @@ -835,28 +754,23 @@ MDXContent.__RSPRESS_PAGE_META["index.mdx"] = {"toc":[],"title":"","headingTitle exports[`remark-container > end with an inline code 1`] = ` "import {jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime"; import {useMDXComponents as _provideComponents} from "@mdx-js/react"; +import {Callout as $$$callout$$$} from "@theme"; function _createMdxContent(props) { const _components = { code: "code", - div: "div", p: "p", ..._provideComponents(), ...props.components }; - return _jsxs(_components.div, { - className: "rspress-directive tip", - children: [_jsx(_components.div, { - className: "rspress-directive-title", - children: "TIP" - }), _jsxs(_components.div, { - className: "rspress-directive-content", - children: [_jsx(_components.p, { - children: "\\nLine 1." - }), _jsxs(_components.p, { - children: ["Line 2 with ", _jsx(_components.code, { - children: "code" - }), "."] - })] + return _jsxs($$$callout$$$, { + type: "tip", + title: "Tip", + children: [_jsx(_components.p, { + children: "\\nLine 1." + }), _jsxs(_components.p, { + children: ["Line 2 with ", _jsx(_components.code, { + children: "code" + }), "."] })] }); } @@ -882,67 +796,62 @@ MDXContent.__RSPRESS_PAGE_META["index.mdx"] = {"toc":[],"title":"","headingTitle exports[`remark-container > github alerts ~ caution 1`] = ` "import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime"; import {useMDXComponents as _provideComponents} from "@mdx-js/react"; +import {Callout as $$$callout$$$} from "@theme"; function _createMdxContent(props) { const _components = { code: "code", - div: "div", p: "p", pre: "pre", span: "span", ..._provideComponents(), ...props.components }; - return _jsxs(_components.div, { - className: "rspress-directive caution", - children: [_jsx(_components.div, { - className: "rspress-directive-title", - children: "CAUTION" - }), _jsxs(_components.div, { - className: "rspress-directive-content", - children: [_jsx(_components.p, { - children: "Use this code:-" - }), _jsx(_Fragment, { - children: _jsx(_components.pre, { - className: "shiki css-variables", - style: { - backgroundColor: "var(--shiki-background)", - color: "var(--shiki-foreground)" - }, - tabIndex: "0", - children: _jsx(_components.code, { - className: "language-javascript", - children: _jsxs(_components.span, { - className: "line", - children: [_jsx(_components.span, { - style: { - color: "var(--shiki-token-constant)" - }, - children: "console" - }), _jsx(_components.span, { - style: { - color: "var(--shiki-token-function)" - }, - children: ".log" - }), _jsx(_components.span, { - style: { - color: "var(--shiki-foreground)" - }, - children: "(" - }), _jsx(_components.span, { - style: { - color: "var(--shiki-token-constant)" - }, - children: "69" - }), _jsx(_components.span, { - style: { - color: "var(--shiki-foreground)" - }, - children: ");" - })] - }) + return _jsxs($$$callout$$$, { + type: "caution", + title: "CAUTION", + children: [_jsx(_components.p, { + children: "Use this code:-" + }), _jsx(_Fragment, { + children: _jsx(_components.pre, { + className: "shiki css-variables", + style: { + backgroundColor: "var(--shiki-background)", + color: "var(--shiki-foreground)" + }, + tabIndex: "0", + children: _jsx(_components.code, { + className: "language-javascript", + children: _jsxs(_components.span, { + className: "line", + children: [_jsx(_components.span, { + style: { + color: "var(--shiki-token-constant)" + }, + children: "console" + }), _jsx(_components.span, { + style: { + color: "var(--shiki-token-function)" + }, + children: ".log" + }), _jsx(_components.span, { + style: { + color: "var(--shiki-foreground)" + }, + children: "(" + }), _jsx(_components.span, { + style: { + color: "var(--shiki-token-constant)" + }, + children: "69" + }), _jsx(_components.span, { + style: { + color: "var(--shiki-foreground)" + }, + children: ");" + })] }) }) - })] + }) })] }); } @@ -968,31 +877,26 @@ MDXContent.__RSPRESS_PAGE_META["index.mdx"] = {"toc":[],"title":"","headingTitle exports[`remark-container > github alerts ~ note 1`] = ` "import {jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime"; import {useMDXComponents as _provideComponents} from "@mdx-js/react"; +import {Callout as $$$callout$$$} from "@theme"; function _createMdxContent(props) { const _components = { a: "a", - div: "div", h1: "h1", ..._provideComponents(), ...props.components }; - return _jsxs(_components.div, { - className: "rspress-directive note", - children: [_jsx(_components.div, { - className: "rspress-directive-title", - children: "NOTE" - }), _jsx(_components.div, { - className: "rspress-directive-content", - children: _jsxs(_components.h1, { - id: "please-read-this-note", - children: [_jsx(_components.a, { - className: "header-anchor", - "aria-hidden": "true", - href: "#please-read-this-note", - children: "#" - }), "Please read this note!"] - }) - })] + return _jsx($$$callout$$$, { + type: "note", + title: "NOTE", + children: _jsxs(_components.h1, { + id: "please-read-this-note", + children: [_jsx(_components.a, { + className: "rp-header-anchor", + "aria-hidden": "true", + href: "#please-read-this-note", + children: "#" + }), "Please read this note!"] + }) }); } export default function MDXContent(props = {}) { @@ -1015,29 +919,24 @@ MDXContent.__RSPRESS_PAGE_META["index.mdx"] = {"toc":[],"title":"","headingTitle `; exports[`remark-container > github alerts ~ tip 1`] = ` -"import {jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime"; +"import {jsx as _jsx} from "react/jsx-runtime"; import {useMDXComponents as _provideComponents} from "@mdx-js/react"; +import {Callout as $$$callout$$$} from "@theme"; function _createMdxContent(props) { const _components = { - div: "div", p: "p", strong: "strong", ..._provideComponents(), ...props.components }; - return _jsxs(_components.div, { - className: "rspress-directive tip", - children: [_jsx(_components.div, { - className: "rspress-directive-title", - children: "TIP" - }), _jsx(_components.div, { - className: "rspress-directive-content", - children: _jsx(_components.p, { - children: _jsx(_components.strong, { - children: "Helpful advice for doing things better or more easily." - }) + return _jsx($$$callout$$$, { + type: "tip", + title: "TIP", + children: _jsx(_components.p, { + children: _jsx(_components.strong, { + children: "Helpful advice for doing things better or more easily." }) - })] + }) }); } export default function MDXContent(props = {}) { @@ -1062,29 +961,24 @@ MDXContent.__RSPRESS_PAGE_META["index.mdx"] = {"toc":[],"title":"","headingTitle exports[`remark-container > github alerts ~ warning 1`] = ` "import {jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime"; import {useMDXComponents as _provideComponents} from "@mdx-js/react"; +import {Callout as $$$callout$$$} from "@theme"; function _createMdxContent(props) { const _components = { code: "code", - div: "div", p: "p", ..._provideComponents(), ...props.components }; - return _jsxs(_components.div, { - className: "rspress-directive warning", - children: [_jsx(_components.div, { - className: "rspress-directive-title", - children: "WARNING" - }), _jsx(_components.div, { - className: "rspress-directive-content", - children: _jsxs(_components.p, { - children: ["Use ", _jsx(_components.code, { - children: "dummy" - }), " instead of ", _jsx(_components.code, { - children: "demo" - })] - }) - })] + return _jsx($$$callout$$$, { + type: "warning", + title: "WARNING", + children: _jsxs(_components.p, { + children: ["Use ", _jsx(_components.code, { + children: "dummy" + }), " instead of ", _jsx(_components.code, { + children: "demo" + })] + }) }); } export default function MDXContent(props = {}) { @@ -1109,9 +1003,9 @@ MDXContent.__RSPRESS_PAGE_META["index.mdx"] = {"toc":[],"title":"","headingTitle exports[`remark-container > nested in list - github alerts 1`] = ` "import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime"; import {useMDXComponents as _provideComponents} from "@mdx-js/react"; +import {Callout as $$$callout$$$} from "@theme"; function _createMdxContent(props) { const _components = { - div: "div", li: "li", ol: "ol", p: "p", @@ -1124,34 +1018,24 @@ function _createMdxContent(props) { children: ["\\n", _jsxs(_components.li, { children: ["\\n", _jsx(_components.p, { children: "Title" - }), "\\n", _jsxs(_components.div, { - className: "rspress-directive tip", - children: [_jsx(_components.div, { - className: "rspress-directive-title", - children: "TIP" - }), _jsx(_components.div, { - className: "rspress-directive-content", - children: _jsx(_components.p, { - children: "This is a 'tip' style block." - }) - })] + }), "\\n", _jsx($$$callout$$$, { + type: "tip", + title: "TIP", + children: _jsx(_components.p, { + children: "This is a 'tip' style block." + }) }), "\\n"] }), "\\n"] }), "\\n", _jsxs(_components.ol, { children: ["\\n", _jsx(_components.li, { children: "Title" }), "\\n"] - }), "\\n", _jsxs(_components.div, { - className: "rspress-directive tip", - children: [_jsx(_components.div, { - className: "rspress-directive-title", - children: "TIP" - }), _jsx(_components.div, { - className: "rspress-directive-content", - children: _jsx(_components.p, { - children: "This is a 'tip' style block." - }) - })] + }), "\\n", _jsx($$$callout$$$, { + type: "tip", + title: "TIP", + children: _jsx(_components.p, { + children: "This is a 'tip' style block." + }) })] }); } @@ -1177,9 +1061,9 @@ MDXContent.__RSPRESS_PAGE_META["index.mdx"] = {"toc":[],"title":"","headingTitle exports[`remark-container > nested in ordered list 1`] = ` "import {jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime"; import {useMDXComponents as _provideComponents} from "@mdx-js/react"; +import {Callout as $$$callout$$$} from "@theme"; function _createMdxContent(props) { const _components = { - div: "div", li: "li", ol: "ol", p: "p", @@ -1190,17 +1074,12 @@ function _createMdxContent(props) { children: ["\\n", _jsxs(_components.li, { children: ["\\n", _jsx(_components.p, { children: "Title1" - }), "\\n", _jsxs(_components.div, { - className: "rspress-directive tip", - children: [_jsx(_components.div, { - className: "rspress-directive-title", - children: "TIP" - }), _jsx(_components.div, { - className: "rspress-directive-content", - children: _jsx(_components.p, { - children: "\\nThis is a tip." - }) - })] + }), "\\n", _jsx($$$callout$$$, { + type: "tip", + title: "Tip", + children: _jsx(_components.p, { + children: "\\nThis is a tip." + }) }), "\\n"] }), "\\n", _jsxs(_components.li, { children: ["\\n", _jsx(_components.p, { @@ -1231,9 +1110,9 @@ MDXContent.__RSPRESS_PAGE_META["index.mdx"] = {"toc":[],"title":"","headingTitle exports[`remark-container > nested in ordered list inside mdx component 1`] = ` "import {jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime"; import {useMDXComponents as _provideComponents} from "@mdx-js/react"; +import {Callout as $$$callout$$$} from "@theme"; function _createMdxContent(props) { const _components = { - div: "div", li: "li", ol: "ol", p: "p", @@ -1247,17 +1126,12 @@ function _createMdxContent(props) { children: ["\\n", _jsxs(_components.li, { children: ["\\n", _jsx(_components.p, { children: "Title1" - }), "\\n", _jsxs(_components.div, { - className: "rspress-directive tip", - children: [_jsx(_components.div, { - className: "rspress-directive-title", - children: "TIP" - }), _jsx(_components.div, { - className: "rspress-directive-content", - children: _jsx(_components.p, { - children: "\\nThis is a tip." - }) - })] + }), "\\n", _jsx($$$callout$$$, { + type: "tip", + title: "Tip", + children: _jsx(_components.p, { + children: "\\nThis is a tip." + }) }), "\\n"] }), "\\n", _jsxs(_components.li, { children: ["\\n", _jsx(_components.p, { @@ -1291,9 +1165,9 @@ MDXContent.__RSPRESS_PAGE_META["index.mdx"] = {"toc":[],"title":"","headingTitle exports[`remark-container > nested in unordered list 1`] = ` "import {jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime"; import {useMDXComponents as _provideComponents} from "@mdx-js/react"; +import {Callout as $$$callout$$$} from "@theme"; function _createMdxContent(props) { const _components = { - div: "div", li: "li", p: "p", ul: "ul", @@ -1304,17 +1178,12 @@ function _createMdxContent(props) { children: ["\\n", _jsxs(_components.li, { children: ["\\n", _jsx(_components.p, { children: "Title1" - }), "\\n", _jsxs(_components.div, { - className: "rspress-directive tip", - children: [_jsx(_components.div, { - className: "rspress-directive-title", - children: "TIP" - }), _jsx(_components.div, { - className: "rspress-directive-content", - children: _jsx(_components.p, { - children: "\\nThis is a tip." - }) - })] + }), "\\n", _jsx($$$callout$$$, { + type: "tip", + title: "Tip", + children: _jsx(_components.p, { + children: "\\nThis is a tip." + }) }), "\\n"] }), "\\n", _jsxs(_components.li, { children: ["\\n", _jsx(_components.p, { @@ -1345,9 +1214,9 @@ MDXContent.__RSPRESS_PAGE_META["index.mdx"] = {"toc":[],"title":"","headingTitle exports[`remark-container > nested in unordered list inside mdx component 1`] = ` "import {jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime"; import {useMDXComponents as _provideComponents} from "@mdx-js/react"; +import {Callout as $$$callout$$$} from "@theme"; function _createMdxContent(props) { const _components = { - div: "div", li: "li", p: "p", ul: "ul", @@ -1361,17 +1230,12 @@ function _createMdxContent(props) { children: ["\\n", _jsxs(_components.li, { children: ["\\n", _jsx(_components.p, { children: "Title1" - }), "\\n", _jsxs(_components.div, { - className: "rspress-directive tip", - children: [_jsx(_components.div, { - className: "rspress-directive-title", - children: "TIP" - }), _jsx(_components.div, { - className: "rspress-directive-content", - children: _jsx(_components.p, { - children: "\\nThis is a tip." - }) - })] + }), "\\n", _jsx($$$callout$$$, { + type: "tip", + title: "Tip", + children: _jsx(_components.p, { + children: "\\nThis is a tip." + }) }), "\\n"] }), "\\n", _jsxs(_components.li, { children: ["\\n", _jsx(_components.p, { @@ -1405,30 +1269,25 @@ MDXContent.__RSPRESS_PAGE_META["index.mdx"] = {"toc":[],"title":"","headingTitle exports[`remark-container > start with a link 1`] = ` "import {jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime"; import {useMDXComponents as _provideComponents} from "@mdx-js/react"; +import {Callout as $$$callout$$$} from "@theme"; function _createMdxContent(props) { const _components = { a: "a", - div: "div", p: "p", ..._provideComponents(), ...props.components }; - return _jsxs(_components.div, { - className: "rspress-directive tip", - children: [_jsx(_components.div, { - className: "rspress-directive-title", - children: "TIP" - }), _jsx(_components.div, { - className: "rspress-directive-content", - children: _jsxs(_components.p, { - children: ["\\n", _jsx(_components.a, { - href: "https://example.com", - rel: "noopener noreferrer", - target: "_blank", - children: "link" - }), " is a link"] - }) - })] + return _jsx($$$callout$$$, { + type: "tip", + title: "Tip", + children: _jsxs(_components.p, { + children: ["\\n", _jsx(_components.a, { + href: "https://example.com", + rel: "noopener noreferrer", + target: "_blank", + children: "link" + }), " is a link"] + }) }); } export default function MDXContent(props = {}) { diff --git a/packages/core/src/node/mdx/remarkPlugins/__snapshots__/fileCodeBlock.test.ts.snap b/packages/core/src/node/mdx/remarkPlugins/__snapshots__/fileCodeBlock.test.ts.snap index 087bc4943..3129bd6dd 100644 --- a/packages/core/src/node/mdx/remarkPlugins/__snapshots__/fileCodeBlock.test.ts.snap +++ b/packages/core/src/node/mdx/remarkPlugins/__snapshots__/fileCodeBlock.test.ts.snap @@ -3,6 +3,7 @@ exports[`remarkFileCodeBlock > basic 1`] = ` "import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime"; import {useMDXComponents as _provideComponents} from "@mdx-js/react"; +import {Callout as $$$callout$$$} from "@theme"; function _createMdxContent(props) { const _components = { code: "code", diff --git a/packages/core/src/node/mdx/remarkPlugins/__snapshots__/image.test.ts.snap b/packages/core/src/node/mdx/remarkPlugins/__snapshots__/image.test.ts.snap index 36e6f8bb5..13ec4c567 100644 --- a/packages/core/src/node/mdx/remarkPlugins/__snapshots__/image.test.ts.snap +++ b/packages/core/src/node/mdx/remarkPlugins/__snapshots__/image.test.ts.snap @@ -5,6 +5,7 @@ exports[`mdx > basic 1`] = ` import {useMDXComponents as _provideComponents} from "@mdx-js/react"; import image0 from "./test3.jpg"; import image1 from "./test4.png"; +import {Callout as $$$callout$$$} from "@theme"; function _createMdxContent(props) { const _components = { img: "img", diff --git a/packages/core/src/node/mdx/remarkPlugins/__snapshots__/link.test.ts.snap b/packages/core/src/node/mdx/remarkPlugins/__snapshots__/link.test.ts.snap index 2fb8c4363..757ea1eff 100644 --- a/packages/core/src/node/mdx/remarkPlugins/__snapshots__/link.test.ts.snap +++ b/packages/core/src/node/mdx/remarkPlugins/__snapshots__/link.test.ts.snap @@ -4,6 +4,7 @@ exports[`mdx > basic 1`] = ` "/*jsx link will not be transformed*/ import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime"; import {useMDXComponents as _provideComponents} from "@mdx-js/react"; +import {Callout as $$$callout$$$} from "@theme"; function _createMdxContent(props) { const _components = { a: "a", diff --git a/packages/core/src/node/mdx/remarkPlugins/builtin.ts b/packages/core/src/node/mdx/remarkPlugins/builtin.ts index 0d9603096..bcf80a8c3 100644 --- a/packages/core/src/node/mdx/remarkPlugins/builtin.ts +++ b/packages/core/src/node/mdx/remarkPlugins/builtin.ts @@ -2,7 +2,7 @@ import path from 'node:path'; import type { Root } from 'mdast'; import type { MdxjsEsm } from 'mdast-util-mdxjs-esm'; import type { Plugin } from 'unified'; -import { getASTNodeImport } from '../../utils'; +import { getDefaultImportAstNode } from '../../utils'; /** * A remark plugin to import all builtin components. @@ -16,7 +16,7 @@ export const remarkBuiltin: Plugin<[{ globalComponents: string[] }], Root> = ({ const demos: MdxjsEsm[] = globalComponents.map(componentPath => { const filename = path.parse(componentPath).name; const componentName = filename[0].toUpperCase() + filename.slice(1); - return getASTNodeImport(componentName, componentPath); + return getDefaultImportAstNode(componentName, componentPath); }); tree.children.unshift(...demos); diff --git a/packages/core/src/node/mdx/remarkPlugins/containerSyntax.ts b/packages/core/src/node/mdx/remarkPlugins/containerSyntax.ts index 4e1bde76f..dea971351 100644 --- a/packages/core/src/node/mdx/remarkPlugins/containerSyntax.ts +++ b/packages/core/src/node/mdx/remarkPlugins/containerSyntax.ts @@ -23,6 +23,7 @@ import type { } from 'mdast'; import type { ContainerDirective } from 'mdast-util-directive'; import type { Plugin } from 'unified'; +import { getNamedImportAstNode } from '../../utils'; export const DIRECTIVE_TYPES = [ 'tip', @@ -39,6 +40,8 @@ export const REGEX_GH_BEGIN = /^\s*\s*\[!(\w+)\]\s*(.*)?/; export const TITLE_REGEX_IN_MD = /{\s*title=["']?(.+)}\s*/; export const TITLE_REGEX_IN_MDX = /\s*title=["']?(.+)\s*/; +const CALLOUT_COMPONENT = '$$$callout$$$'; // in md, we can not add import statement, so we use a special component name to avoid conflict with user components + export type DirectiveType = (typeof DIRECTIVE_TYPES)[number]; const trimTailingQuote = (str: string) => str.replace(/['"]$/g, ''); @@ -50,6 +53,10 @@ const parseTitle = (rawTitle = '', isMDX = false) => { return trimTailingQuote(matched?.[1] || rawTitle); }; +const getTypeName = (type: DirectiveType | string): string => { + return type[0].toUpperCase() + type.slice(1).toLowerCase(); +}; + /** * Construct the DOM structure of the container directive. * For example: @@ -61,7 +68,7 @@ const parseTitle = (rawTitle = '', isMDX = false) => { * will be transformed to: * *
- *
TIP
+ *
Tip
*
*

This is a tip

*
@@ -73,40 +80,21 @@ const createContainer = ( title: string | undefined, children: (BlockContent | PhrasingContent)[], ): ContainerDirective => { - const isDetails = type === 'details'; - - const rootHName = isDetails ? 'details' : 'div'; - const titleHName = isDetails ? 'summary' : 'div'; - return { type: 'containerDirective', - name: type, + name: CALLOUT_COMPONENT, + attributes: { + type: type, + title: title || getTypeName(type), + }, data: { - hName: rootHName, + hName: CALLOUT_COMPONENT, hProperties: { - class: `rspress-directive ${type}`, + type: type, + title: title || getTypeName(type), }, }, - children: [ - { - type: 'paragraph', - data: { - hName: titleHName, - hProperties: { - class: 'rspress-directive-title', - }, - }, - children: [{ type: 'text', value: title || type.toUpperCase() }], - }, - { - type: 'paragraph', - data: { - hName: 'div', - hProperties: { class: 'rspress-directive-content' }, - }, - children: children as PhrasingContent[], - }, - ], + children: children as BlockContent[], }; }; @@ -370,5 +358,10 @@ function transformer(tree: Parent) { } export const remarkContainerSyntax: Plugin<[], Root> = () => { - return transformer; + return tree => { + transformer(tree); + tree.children.unshift( + getNamedImportAstNode('Callout', CALLOUT_COMPONENT, '@theme'), + ); + }; }; diff --git a/packages/core/src/node/mdx/remarkPlugins/image.ts b/packages/core/src/node/mdx/remarkPlugins/image.ts index 00144f908..4356a4a73 100644 --- a/packages/core/src/node/mdx/remarkPlugins/image.ts +++ b/packages/core/src/node/mdx/remarkPlugins/image.ts @@ -4,7 +4,7 @@ import type { Root } from 'mdast'; import type { MdxjsEsm } from 'mdast-util-mdxjs-esm'; import type { Plugin } from 'unified'; import { visit } from 'unist-util-visit'; -import { getASTNodeImport } from '../../utils'; +import { getDefaultImportAstNode } from '../../utils'; const normalizeImageUrl = (imageUrl: string): string => { if (isExternalUrl(imageUrl) || imageUrl.startsWith('/')) { @@ -69,7 +69,7 @@ export const remarkImage: Plugin<[], Root> = () => (tree, _file) => { ].filter(Boolean), }); - images.push(getASTNodeImport(tempVariableName, imagePath)); + images.push(getDefaultImportAstNode(tempVariableName, imagePath)); }); visit(tree, node => { @@ -98,7 +98,7 @@ export const remarkImage: Plugin<[], Root> = () => (tree, _file) => { Object.assign(srcAttr, getMdxSrcAttribute(tempVariableName)); - images.push(getASTNodeImport(tempVariableName, imagePath)); + images.push(getDefaultImportAstNode(tempVariableName, imagePath)); }); tree.children.unshift(...images); diff --git a/packages/core/src/node/utils/getASTNodeImport.ts b/packages/core/src/node/utils/getASTNodeImport.ts deleted file mode 100644 index e1b6696fd..000000000 --- a/packages/core/src/node/utils/getASTNodeImport.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { MdxjsEsm } from 'mdast-util-mdxjs-esm'; - -// Construct import statement for AST -// Such as: import image1 from './test.png' -export const getASTNodeImport = (name: string, from: string) => - ({ - type: 'mdxjsEsm', - value: `import ${name} from ${JSON.stringify(from)}`, - data: { - estree: { - type: 'Program', - sourceType: 'module', - body: [ - { - type: 'ImportDeclaration', - specifiers: [ - { - type: 'ImportDefaultSpecifier', - local: { type: 'Identifier', name }, - }, - ], - source: { - type: 'Literal', - value: from, - raw: `${JSON.stringify(from)}`, - }, - }, - ], - }, - }, - }) as MdxjsEsm; diff --git a/packages/core/src/node/utils/getImportAstNode.ts b/packages/core/src/node/utils/getImportAstNode.ts new file mode 100644 index 000000000..0df50aeda --- /dev/null +++ b/packages/core/src/node/utils/getImportAstNode.ts @@ -0,0 +1,64 @@ +import type { MdxjsEsm } from 'mdast-util-mdxjs-esm'; + +// Construct import statement for AST +// Such as: import image1 from './test.png' +export const getDefaultImportAstNode = (name: string, from: string) => + ({ + type: 'mdxjsEsm', + value: `import ${name} from ${JSON.stringify(from)}`, + data: { + estree: { + type: 'Program', + sourceType: 'module', + body: [ + { + type: 'ImportDeclaration', + specifiers: [ + { + type: 'ImportDefaultSpecifier', + local: { type: 'Identifier', name }, + }, + ], + source: { + type: 'Literal', + value: from, + raw: `${JSON.stringify(from)}`, + }, + }, + ], + }, + }, + }) as MdxjsEsm; + +export const getNamedImportAstNode = ( + name: string, + alias: string, + from: string, +) => + ({ + type: 'mdxjsEsm', + value: `import { ${name} as ${alias} } from ${JSON.stringify(from)}`, + data: { + estree: { + type: 'Program', + sourceType: 'module', + body: [ + { + type: 'ImportDeclaration', + specifiers: [ + { + type: 'ImportSpecifier', + imported: { type: 'Identifier', name }, + local: { type: 'Identifier', name: alias }, + }, + ], + source: { + type: 'Literal', + value: from, + raw: `${JSON.stringify(from)}`, + }, + }, + ], + }, + }, + }) as MdxjsEsm; diff --git a/packages/core/src/node/utils/index.ts b/packages/core/src/node/utils/index.ts index 811262acf..64f20522c 100644 --- a/packages/core/src/node/utils/index.ts +++ b/packages/core/src/node/utils/index.ts @@ -4,6 +4,6 @@ export * from './detectReactVersion'; export * from './escapeHeadingIds'; export * from './flattenMdxContent'; export * from './fs'; -export * from './getASTNodeImport'; +export * from './getImportAstNode'; export * from './getPageKey'; export * from './normalizePath'; diff --git a/packages/core/src/runtime/csrClientEntry.tsx b/packages/core/src/runtime/csrClientEntry.tsx index 33dfd0ed7..90df91687 100644 --- a/packages/core/src/runtime/csrClientEntry.tsx +++ b/packages/core/src/runtime/csrClientEntry.tsx @@ -1,5 +1,5 @@ import { createRoot } from 'react-dom/client'; import { ClientApp } from './ClientApp'; -const container = document.getElementById('root')!; +const container = document.getElementById('__rspress_root')!; createRoot(container).render(); diff --git a/packages/core/src/runtime/ssrClientEntry.tsx b/packages/core/src/runtime/ssrClientEntry.tsx index e5465e92a..f940c9094 100644 --- a/packages/core/src/runtime/ssrClientEntry.tsx +++ b/packages/core/src/runtime/ssrClientEntry.tsx @@ -10,7 +10,7 @@ import { initPageData } from './initPageData'; // 3. add onRecoverableError async function renderInBrowser() { - const container = document.getElementById('root')!; + const container = document.getElementById('__rspress_root')!; const initialPageData = await initPageData( removeBase(window.location.pathname), ); diff --git a/packages/plugin-algolia/src/runtime/Search.css b/packages/plugin-algolia/src/runtime/Search.css index c637f0623..84c7eb22e 100644 --- a/packages/plugin-algolia/src/runtime/Search.css +++ b/packages/plugin-algolia/src/runtime/Search.css @@ -1,52 +1,40 @@ -.DocSearch { - align-self: center; -} - [class*='DocSearch'] { --docsearch-primary-color: var(--rp-c-brand); --docsearch-text-color: var(--rp-c-text-1); --docsearch-secondary-text-color: var(--rp-c-text-2); --docsearch-muted-color: var(--rp-c-text-2); - --docsearch-subtle-color: var(--rp-c-divider); + --docsearch-subtle-color: var(--rp-c-divider-light); --docsearch-container-background: rgba(60, 60, 60, 0.4); --docsearch-modal-background: var(--rp-c-bg); --docsearch-searchbox-background: var(--rp-c-bg-soft); --docsearch-searchbox-focus-background: var(--rp-c-bg); - --docsearch-hit-color: var(--rp-c-text-1); + + --docsearch-hit-highlight-color: var(--rp-c-brand-tint); --docsearch-hit-active-color: var(--rp-c-text-1); --docsearch-hit-background: var(--rp-c-bg-soft); --docsearch-hit-shadow: var(--rp-shadow-2); - --docsearch-footer-background: var(--rp-c-bg-soft); + + --docsearch-footer-background: var(--rp-c-bg); --docsearch-key-background: var(--rp-c-bg-mute); --docsearch-key-color: var(--rp-c-text-2); - --docsearch-logo-color: var(--rp-c-text-2); - --docsearch-highlight-color: var(--rp-c-brand); - --docsearch-icon-color: var(--rp-c-text-2); - --docsearch-background-color: var(--rp-c-bg); -} -html.rp-dark [class*='DocSearch'] { - --docsearch-container-background: rgba(0, 0, 0, 0.8); + --docsearch-highlight-color: var(--rp-c-brand); + --docsearch-icon-color: var(--rp-c-text-1); + --docsearch-background-color: var(--rp-c-bg-mute); } -@media (max-width: 768px) { - .DocSearch { - --docsearch-searchbox-background: transparent; - } +.DocSearch { + border-radius: var(--rp-radius-small); + --docsearch-searchbox-background: color-mix( + in srgb, + var(--rp-c-bg) 30%, + transparent + ); } .DocSearch-Button-Container > .DocSearch-Button-Placeholder { font-size: 0.825rem; } - -span.DocSearch-Button-Keys > kbd { - font-size: 0.825rem; -} - -.DocSearch-Screen-Icon { - display: flex; - justify-content: center; -} diff --git a/packages/plugin-api-docgen/static/global-components/API.tsx b/packages/plugin-api-docgen/static/global-components/API.tsx index 08b09ddac..cb9f0e36e 100644 --- a/packages/plugin-api-docgen/static/global-components/API.tsx +++ b/packages/plugin-api-docgen/static/global-components/API.tsx @@ -60,7 +60,7 @@ function create(node: Element): Element { type: 'element', tagName: 'a', properties: { - class: 'header-anchor', + class: 'rp-header-anchor', ariaHidden: 'true', href: `#${node.properties!.id}`, }, diff --git a/packages/plugin-preview/src/index.ts b/packages/plugin-preview/src/index.ts index e33b17725..7d6c62ede 100644 --- a/packages/plugin-preview/src/index.ts +++ b/packages/plugin-preview/src/index.ts @@ -109,7 +109,7 @@ export function pluginPreview(options?: Options): RspressPlugin { if (Object.keys(sourceEntry).length === 0) { return; } - const { html, source, output, performance } = clientConfig ?? {}; + const { source, output, performance } = clientConfig ?? {}; const rsbuildConfig = mergeRsbuildConfig( { server: { @@ -125,7 +125,6 @@ export function pluginPreview(options?: Options): RspressPlugin { printFileSize: false, buildCache: false, }, - html, source: { ...source, entry: sourceEntry, diff --git a/packages/plugin-twoslash/src/index.ts b/packages/plugin-twoslash/src/index.ts index 926ae9d4a..d66fffde9 100644 --- a/packages/plugin-twoslash/src/index.ts +++ b/packages/plugin-twoslash/src/index.ts @@ -143,11 +143,12 @@ export function pluginTwoslash(options?: PluginTwoslashOptions): RspressPlugin { class: 'twoslash-popup-trigger', }, }, - completionPopup: { - properties: { - class: 'twoslash-popup-inner', - }, - }, + // TODO: css changes + // completionPopup: { + // properties: { + // class: 'twoslash-popup-inner', + // }, + // }, completionCompose: ({ cursor, popup }) => [ cursor, { diff --git a/packages/runtime/src/hooks/useActiveMatcher.ts b/packages/runtime/src/hooks/useActiveMatcher.ts new file mode 100644 index 000000000..dae6a9d21 --- /dev/null +++ b/packages/runtime/src/hooks/useActiveMatcher.ts @@ -0,0 +1,18 @@ +import { useCallback, useRef } from 'react'; +import { useLocation } from 'react-router-dom'; +import { isActive } from '../route'; + +export const useActiveMatcher = () => { + const { pathname: rawPathname } = useLocation(); + + const ref = useRef(rawPathname); + ref.current = rawPathname; + + const activeMatcher = useCallback((link: string) => { + const rawPathname = ref.current; + const pathname = decodeURIComponent(rawPathname); + return isActive(link, pathname); + }, []); + + return activeMatcher; +}; diff --git a/packages/runtime/src/hooks/useSidebar.ts b/packages/runtime/src/hooks/useSidebar.ts index db76e448b..d7d9230fe 100644 --- a/packages/runtime/src/hooks/useSidebar.ts +++ b/packages/runtime/src/hooks/useSidebar.ts @@ -1,10 +1,15 @@ import { matchSidebar, type NormalizedSidebar, + type NormalizedSidebarGroup, type SidebarData, + type SidebarDivider, + type SidebarItem, + type SidebarSectionHeader, } from '@rspress/shared'; -import { useMemo } from 'react'; +import { useLayoutEffect, useMemo, useState } from 'react'; import { useLocation } from 'react-router-dom'; +import { useActiveMatcher } from './useActiveMatcher'; import { useLocaleSiteData } from './useLocaleSiteData'; /** @@ -50,3 +55,78 @@ export function useSidebar(): SidebarData { return sidebarData; } + +function createInitialSidebar( + rawSidebarData: SidebarData, + activeMatcher: (link: string) => boolean, +) { + const matchCache = new WeakMap< + | NormalizedSidebarGroup + | SidebarItem + | SidebarDivider + | SidebarSectionHeader, + boolean + >(); + const match = ( + item: + | NormalizedSidebarGroup + | SidebarItem + | SidebarDivider + | SidebarSectionHeader, + ) => { + if (matchCache.has(item)) { + return matchCache.get(item); + } + if ('link' in item && item.link && activeMatcher(item.link)) { + matchCache.set(item, true); + return true; + } + if ('items' in item) { + const result = item.items.some(child => match(child)); + if (result) { + matchCache.set(item, true); + return true; + } + } + matchCache.set(item, false); + return false; + }; + const traverse = ( + item: + | NormalizedSidebarGroup + | SidebarItem + | SidebarDivider + | SidebarSectionHeader, + ) => { + if ('items' in item) { + item.items.forEach(traverse); + if (match(item)) { + item.collapsed = false; + } + } + }; + const newSidebarData = rawSidebarData.filter(Boolean).flat(); + newSidebarData.forEach(traverse); + return newSidebarData; +} + +/** + * handle the collapsed state of the sidebar groups + */ +export function useSidebarDynamic(): [ + SidebarData, + React.Dispatch>, +] { + const rawSidebarData = useSidebar(); + const activeMatcher = useActiveMatcher(); + + const [sidebar, setSidebar] = useState(() => + createInitialSidebar(rawSidebarData, activeMatcher), + ); + + useLayoutEffect(() => { + setSidebar(createInitialSidebar(rawSidebarData, activeMatcher)); + }, [rawSidebarData]); + + return [sidebar, setSidebar]; +} diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts index 7dd7acc79..dcd9a9d95 100644 --- a/packages/runtime/src/index.ts +++ b/packages/runtime/src/index.ts @@ -1,7 +1,7 @@ export { Head } from '@unhead/react'; -export { createPortal, flushSync } from 'react-dom'; export * from 'react-router-dom'; export { Content } from './Content'; +export { useActiveMatcher } from './hooks/useActiveMatcher'; export { ThemeContext, useDark } from './hooks/useDark'; export { useFrontmatter } from './hooks/useFrontmatter'; export { useI18n } from './hooks/useI18n'; @@ -11,13 +11,17 @@ export { useNav } from './hooks/useNav'; export { PageContext, usePage } from './hooks/usePage'; export { usePageData } from './hooks/usePageData'; export { usePages } from './hooks/usePages'; -export { getSidebarDataGroup, useSidebar } from './hooks/useSidebar'; +export { + getSidebarDataGroup, + useSidebar, + useSidebarDynamic, +} from './hooks/useSidebar'; export { useSite } from './hooks/useSite'; export { useVersion } from './hooks/useVersion'; export { useWindowSize } from './hooks/useWindowSize'; export { NoSSR } from './NoSSR'; -export { isActive, pathnameToRouteService } from './route'; +export { isActive, pathnameToRouteService, preloadLink } from './route'; export { addLeadingSlash, cleanUrlByConfig, diff --git a/packages/runtime/src/route.ts b/packages/runtime/src/route.ts index ad1261f9e..28e26a66b 100644 --- a/packages/runtime/src/route.ts +++ b/packages/runtime/src/route.ts @@ -45,3 +45,10 @@ export function isActive(itemLink: string, currentPathname: string): boolean { const linkMatched = matchPath(normalizedItemLink, normalizedCurrentPathname); return linkMatched !== null; } + +export const preloadLink = (link: string) => { + const route = pathnameToRouteService(link); + if (route) { + route.preload(); + } +}; diff --git a/packages/shared/src/types/defaultTheme.ts b/packages/shared/src/types/defaultTheme.ts index 4f6e8796d..c4119540c 100644 --- a/packages/shared/src/types/defaultTheme.ts +++ b/packages/shared/src/types/defaultTheme.ts @@ -93,10 +93,6 @@ export interface Config { * The text of overview filter */ overview?: FilterConfig; - /** - * The behavior of hiding navbar - */ - hideNavbar?: 'always' | 'auto' | 'never'; /** * Whether to enable view transition animation for pages switching */ @@ -106,11 +102,6 @@ export interface Config { * @default false */ enableAppearanceAnimation?: boolean; - /** - * Enable scroll to top button on documentation - * @default false - */ - enableScrollToTop?: boolean; /** * Whether to redirect to the closest locale when the user visits the site * @default 'auto' diff --git a/packages/shared/src/types/index.ts b/packages/shared/src/types/index.ts index 71d990ae6..bc4e87020 100644 --- a/packages/shared/src/types/index.ts +++ b/packages/shared/src/types/index.ts @@ -291,12 +291,12 @@ export type RemotePageInfo = PageIndexInfo & { }; export interface Hero { - name: string; - text: string; - tagline: string; + name?: string; + text?: string; + tagline?: string; image?: { - src: string | { dark: string; light: string }; - alt: string; + src?: string | { dark: string; light: string }; + alt?: string; /** * `srcset` and `sizes` are attributes of `` tag. Please refer to https://mdn.io/srcset for the usage. * When the value is an array, rspress will join array members with commas. @@ -304,7 +304,7 @@ export interface Hero { sizes?: string | string[]; srcset?: string | string[]; }; - actions: { + actions?: { text: string; link: string; theme: 'brand' | 'alt'; diff --git a/packages/theme-default/package.json b/packages/theme-default/package.json index ddc7de83f..b74684da7 100644 --- a/packages/theme-default/package.json +++ b/packages/theme-default/package.json @@ -32,7 +32,7 @@ ], "scripts": { "build": "rslib build", - "dev": "rslib build -w", + "dev": "IS_DEV=1 rslib build -w", "reset": "rimraf ./**/node_modules" }, "dependencies": { @@ -41,6 +41,7 @@ "@rspress/shared": "workspace:*", "@unhead/react": "^2.0.19", "body-scroll-lock": "4.0.0-beta.0", + "clsx": "2.1.1", "copy-to-clipboard": "^3.3.3", "flexsearch": "0.7.43", "github-slugger": "^2.0.0", @@ -49,6 +50,7 @@ "nprogress": "^0.2.0", "react": "^19.1.1", "react-dom": "^19.1.1", + "scroll-into-view-if-needed": "^3.1.0", "shiki": "^3.12.2" }, "devDependencies": { diff --git a/packages/theme-default/rslib.config.ts b/packages/theme-default/rslib.config.ts index bc4c322f0..6fa9fc143 100644 --- a/packages/theme-default/rslib.config.ts +++ b/packages/theme-default/rslib.config.ts @@ -24,9 +24,7 @@ export default defineConfig({ { format: 'esm', bundle: false, - dts: { - bundle: true, - }, + dts: true, plugins: [ pluginReact(), pluginSvgr({ svgrOptions: { exportType: 'default' } }), @@ -51,6 +49,7 @@ export default defineConfig({ }, }, output: { + cleanDistPath: process.env.IS_DEV === '1', target: 'web', externals: COMMON_EXTERNALS, cssModules: { diff --git a/packages/theme-default/src/assets/moon.svg b/packages/theme-default/src/assets/moon.svg index 228c56f68..f13dd85fe 100644 --- a/packages/theme-default/src/assets/moon.svg +++ b/packages/theme-default/src/assets/moon.svg @@ -1,3 +1,12 @@ - + + + + + + + + + + \ No newline at end of file diff --git a/packages/theme-default/src/assets/search.svg b/packages/theme-default/src/assets/search.svg index 685134a7b..56314ddd0 100644 --- a/packages/theme-default/src/assets/search.svg +++ b/packages/theme-default/src/assets/search.svg @@ -1 +1,3 @@ - + + + diff --git a/packages/theme-default/src/assets/small-menu.svg b/packages/theme-default/src/assets/small-menu.svg index be39d45f3..93f2ba9e1 100644 --- a/packages/theme-default/src/assets/small-menu.svg +++ b/packages/theme-default/src/assets/small-menu.svg @@ -1 +1,5 @@ - + + + + + diff --git a/packages/theme-default/src/assets/sun.svg b/packages/theme-default/src/assets/sun.svg index f07ba4360..eabbd893a 100644 --- a/packages/theme-default/src/assets/sun.svg +++ b/packages/theme-default/src/assets/sun.svg @@ -1,11 +1,5 @@ - + + + \ No newline at end of file diff --git a/packages/theme-default/src/components/Aside/ScrollToTop.scss b/packages/theme-default/src/components/Aside/ScrollToTop.scss new file mode 100644 index 000000000..69a4f41e5 --- /dev/null +++ b/packages/theme-default/src/components/Aside/ScrollToTop.scss @@ -0,0 +1,13 @@ +.rp-aside__scroll-to-top { + cursor: pointer; + font-size: 0.875rem; + font-style: normal; + font-weight: 400; + line-height: 20px; + color: var(--rp-c-text-2); + background: transparent; + display: flex; + align-items: center; + gap: 8px; + border: none; +} diff --git a/packages/theme-default/src/components/Aside/ScrollToTop.tsx b/packages/theme-default/src/components/Aside/ScrollToTop.tsx new file mode 100644 index 000000000..1472998d9 --- /dev/null +++ b/packages/theme-default/src/components/Aside/ScrollToTop.tsx @@ -0,0 +1,48 @@ +import './ScrollToTop.scss'; + +export function ScrollToTop() { + const scrollToTop = () => { + window.scrollTo({ + top: 0, + behavior: 'smooth', + }); + }; + + return ( + + ); +} diff --git a/packages/theme-default/src/components/Aside/index.scss b/packages/theme-default/src/components/Aside/index.scss index b5c908463..de775d9ab 100644 --- a/packages/theme-default/src/components/Aside/index.scss +++ b/packages/theme-default/src/components/Aside/index.scss @@ -1,24 +1,44 @@ -.aside-link { - padding: 0.25rem 0; - margin-bottom: 1px; - border-radius: var(--rp-radius-small) 0 0 var(--rp-radius-small); +.rp-aside { + display: flex; + flex-direction: column; + border-left: 1px solid var(--rp-c-divider-light); + padding-left: 20px; + padding-right: 20px; - &:hover { - background-color: var(--rp-c-bg-mute); + &__title { + font-size: 14px; + font-weight: 700; + height: 32px; + line-height: 32px; + font-family: Inter; + font-style: normal; + color: var(--rp-c-text-1); + margin-bottom: 4px; + display: inline-flex; + align-items: center; + gap: 4px; + @media (max-width: 1280px) { + display: none; + } } - &.aside-active { - &, - &:hover { - color: var(--rp-c-link); - background-color: var(--rp-c-brand-tint); - } + &__divider { + height: 1px; + background: var(--rp-c-divider-light); + margin-top: 16px; + margin-bottom: 16px; } -} -.aside-link-text { - padding: 0px 12px; - font-size: 0.8125rem; - line-height: 1.25rem; - overflow-wrap: break-word; + &__toc { + display: flex; + flex-direction: column; + flex: 1; + gap: 6px; + + // allow scroll when there are too many toc items + @media (max-width: 1280px) { + max-height: 60vh; + overflow: auto scroll; + } + } } diff --git a/packages/theme-default/src/components/Aside/index.tsx b/packages/theme-default/src/components/Aside/index.tsx index 70a83be0f..ef510ff1e 100644 --- a/packages/theme-default/src/components/Aside/index.tsx +++ b/packages/theme-default/src/components/Aside/index.tsx @@ -1,99 +1,53 @@ -import { useLocation } from '@rspress/runtime'; -import type { Header } from '@rspress/shared'; -import { useEffect, useMemo } from 'react'; -import { scrollToTarget, useBindingAsideScroll } from '../../logic/sideEffects'; -import { useUISwitch } from '../../logic/useUISwitch.js'; -import { - parseInlineMarkdownText, - renderInlineMarkdown, -} from '../../logic/utils'; - +import { useLocaleSiteData, useLocation, useSite } from '@rspress/runtime'; +import { Toc } from '@theme'; +import { useEffect } from 'react'; +import { scrollToTarget } from '../../logic/sideEffects'; +import { ReadPercent } from '../ReadPercent'; import './index.scss'; -import { useDynamicToc } from './useDynamicToc'; - -const TocItem = ({ - header, - baseHeaderLevel, -}: { - header: Header; - baseHeaderLevel: number; -}) => { - return ( -
  • - { - e.preventDefault(); - window.location.hash = header.id; - }} - > - - -
  • - ); -}; +import { useDynamicToc } from '../Toc/useDynamicToc'; +import { ScrollToTop } from './ScrollToTop'; -export function Aside({ outlineTitle }: { outlineTitle: string }) { - const { scrollPaddingTop } = useUISwitch(); - const headers = useDynamicToc(); +export function Aside() { + const localesData = useLocaleSiteData(); + const { + site: { themeConfig }, + } = useSite(); + const outlineTitle = + localesData?.outlineTitle || themeConfig?.outlineTitle || 'ON THIS PAGE'; - // For outline text highlight - const baseHeaderLevel = 2; + const { pathname } = useLocation(); - const { hash: locationHash = '', pathname } = useLocation(); - const decodedHash: string = useMemo( - () => decodeURIComponent(locationHash), - [locationHash], - ); - - useBindingAsideScroll(headers); - - // why window.scrollTo(0, 0)? - // when using history.scrollRestoration = 'auto' ref: "useUISwitch.ts", we scroll to the last page's position when navigating to nextPage useEffect(() => { + const decodedHash = decodeURIComponent(window.location.hash); if (decodedHash.length === 0) { window.scrollTo(0, 0); } else { const target = document.getElementById(decodedHash.slice(1)); if (target) { - scrollToTarget(target, false, scrollPaddingTop); + scrollToTarget(target, false, 0); + target.scrollIntoView(); } } - }, [decodedHash, headers, pathname]); + }, [pathname]); + + const headers = useDynamicToc(); if (headers.length === 0) { return <>; } return ( -
    -
    -
    - {outlineTitle} -
    - +
    +
    + {outlineTitle} + +
    + +
    +
    +
    ); diff --git a/packages/theme-default/src/components/Aside/processTitleElement.ts b/packages/theme-default/src/components/Aside/processTitleElement.ts index 6d0b41578..d34fe67b8 100644 --- a/packages/theme-default/src/components/Aside/processTitleElement.ts +++ b/packages/theme-default/src/components/Aside/processTitleElement.ts @@ -6,15 +6,17 @@ export function processTitleElement(element: Element): Element { const elementClone = element.cloneNode(true) as Element; - // 1. remove .header-anchor - const anchorElement = elementClone.querySelector('.header-anchor'); + // 1. remove .rp-header-anchor + const anchorElement = elementClone.querySelector('.rp-header-anchor'); if (anchorElement) { elementClone.removeChild(anchorElement); } // 2. delete ".rspress-toc-exclude" element - const excludeElements = elementClone.querySelectorAll('.rspress-toc-exclude'); + const excludeElements = elementClone.querySelectorAll( + '.rspress-toc-exclude,.rp-toc-exclude', + ); excludeElements.forEach(excludeElement => { const parentElement = excludeElement.parentElement; if (parentElement) { diff --git a/packages/theme-default/src/components/Badge/index.module.scss b/packages/theme-default/src/components/Badge/index.module.scss index 272c0f0dc..be8fc0b6f 100644 --- a/packages/theme-default/src/components/Badge/index.module.scss +++ b/packages/theme-default/src/components/Badge/index.module.scss @@ -1,6 +1,18 @@ .badge { font-weight: 500; transition: color 0.25s; + border-radius: var(--rp-radius-small); + + height: 1.5rem; // 24px + display: inline-flex; + justify-content: center; + align-items: center; + gap: 0.25rem; + + // text + font-weight: 500; + font-size: 0.75rem; // 12px + padding: 0.5rem; &.tip { color: var(--rp-container-tip-text); @@ -23,7 +35,7 @@ } &.outline { - border: 1px solid; + border: 1px solid currentColor; &.tip { border-color: var(--rp-container-tip-border); diff --git a/packages/theme-default/src/components/Badge/index.tsx b/packages/theme-default/src/components/Badge/index.tsx index 0e2686a4e..ab7fdcfd4 100644 --- a/packages/theme-default/src/components/Badge/index.tsx +++ b/packages/theme-default/src/components/Badge/index.tsx @@ -1,3 +1,4 @@ +import clsx from 'clsx'; import * as styles from './index.module.scss'; interface BadgeProps { @@ -52,11 +53,11 @@ export function Badge({ return ( {content} diff --git a/packages/theme-default/src/components/Callout/index.scss b/packages/theme-default/src/components/Callout/index.scss new file mode 100644 index 000000000..eff7c854c --- /dev/null +++ b/packages/theme-default/src/components/Callout/index.scss @@ -0,0 +1,262 @@ +/* + * Container Style + */ +:root { + --rp-container-note-border: var(--rp-c-divider-light); + --rp-container-note-text: var(--rp-c-text-1); + --rp-container-note-bg: var(--rp-c-bg-soft); + --rp-container-note-code-bg: rgba(128, 128, 128, 0.1); + --rp-container-note-link: var(--rp-c-link); + + --rp-container-tip-border: rgba(7, 156, 112, 0.2); + --rp-container-tip-text: #008555; + --rp-container-tip-bg: #f2f9f7; + --rp-container-tip-code-bg: rgba(7, 156, 112, 0.1); + + --rp-container-info-border: rgba(0, 149, 255, 0.2); + --rp-container-info-text: #07f; + --rp-container-info-bg: rgba(0, 149, 255, 0.06); + --rp-container-info-code-bg: rgba(0, 149, 255, 0.1); + + --rp-container-warning-border: rgba(255, 197, 23, 0.4); + --rp-container-warning-text: #887233; + --rp-container-warning-bg: rgba(255, 197, 23, 0.1); + --rp-container-warning-code-bg: rgba(255, 197, 23, 0.1); + + --rp-container-danger-border: rgba(237, 60, 80, 0.2); + --rp-container-danger-text: #ab2131; + --rp-container-danger-bg: rgba(237, 60, 80, 0.08); + --rp-container-danger-code-bg: rgba(237, 60, 80, 0.1); + + --rp-container-details-border: var(--rp-c-divider-light); + --rp-container-details-text: var(--rp-c-text-1); + --rp-container-details-bg: var(--rp-c-bg-soft); + --rp-container-details-code-bg: rgba(128, 128, 128, 0.1); + + --rp-code-title-bg-with-opacity: color-mix( + in srgb, + var(--rp-code-title-bg) 70%, + transparent + ); + --rp-code-block-bg-with-opacity: color-mix( + in srgb, + var(--rp-code-block-bg) 70%, + transparent + ); +} + +.dark { + --rp-container-tip-text: #3ec480; + --rp-container-tip-bg: rgba(7, 156, 112, 0.1); + + --rp-container-info-text: #66c2ff; + --rp-container-info-bg: rgba(0, 149, 255, 0.1); + + --rp-container-warning-text: rgb(251, 180, 81); + --rp-container-warning-border: rgba(255, 197, 23, 0.25); + --rp-container-warning-bg: rgba(255, 197, 23, 0.12); + + --rp-container-danger-text: rgb(247, 110, 133); + --rp-container-danger-border: rgba(237, 60, 80, 0.3); + --rp-container-danger-bg: rgba(237, 60, 80, 0.12); +} + +.rp-callout { + border: 1px solid transparent; + border-radius: var(--rp-radius); + padding: 20px 24px 12px 24px; + margin: 24px 0; + + &__title { + margin-bottom: 8px; + font-weight: 600; + font-size: 16px; + position: relative; + + &::before { + content: ''; + position: absolute; + top: -5%; + left: -24px; + width: 4px; + height: 110%; + border-radius: 0 20px 20px 0; + } + } + + &__content { + font-size: 14px; + font-weight: 400; + + p { + margin: 8px 0; + } + + // code + .rspress-code-title { + background-color: var(--rp-code-title-bg-with-opacity); + } + + .rspress-code-content { + background-color: var(--rp-code-block-bg-with-opacity); + } + + a { + font-weight: 500; + transition: color 0.25s; + border-bottom: 1px solid currentColor; + } + } +} + +// note +.rp-callout--note { + border-color: var(--rp-container-note-border); + background-color: var(--rp-container-note-bg); + + .rp-callout__title { + color: var(--rp-container-note-text); + &::before { + background-color: var(--rp-container-note-text); + } + } + + // inline code + :not(pre, div) > code { + color: var(--rp-container-note-text); + background-color: var(--rp-container-note-code-bg); + } + + a { + color: var(--rp-container-note-text); + } +} + +// tip +.rp-callout--tip { + border-color: var(--rp-container-tip-border); + background-color: var(--rp-container-tip-bg); + + .rp-callout__title { + color: var(--rp-container-tip-text); + &::before { + background-color: var(--rp-container-tip-text); + } + } + + // inline code + :not(pre, div) > code { + color: var(--rp-container-tip-text); + background-color: var(--rp-container-tip-code-bg); + } + + a { + color: var(--rp-container-tip-text); + } +} + +// info +.rp-callout--info { + border-color: var(--rp-container-info-border); + background-color: var(--rp-container-info-bg); + + .rp-callout__title { + color: var(--rp-container-info-text); + &::before { + background-color: var(--rp-container-info-text); + } + } + + // inline code + :not(pre, div) > code { + color: var(--rp-container-info-text); + background-color: var(--rp-container-info-code-bg); + } + + a { + color: var(--rp-container-info-text); + } +} + +// warning +.rp-callout--warning { + border-color: var(--rp-container-warning-border); + background-color: var(--rp-container-warning-bg); + + .rp-callout__title { + color: var(--rp-container-warning-text); + &::before { + background-color: var(--rp-container-warning-text); + } + } + + // inline code + :not(pre, div) > code { + color: var(--rp-container-warning-text); + background-color: var(--rp-container-warning-code-bg); + } + + a { + color: var(--rp-container-warning-text); + } +} + +// caution danger +.rp-callout--caution, +.rp-callout--danger { + border-color: var(--rp-container-danger-border); + background-color: var(--rp-container-danger-bg); + + .rp-callout__title { + color: var(--rp-container-danger-text); + &::before { + background-color: var(--rp-container-danger-text); + } + } + + // inline code + :not(pre, div) > code { + color: var(--rp-container-danger-text); + background-color: var(--rp-container-danger-code-bg); + } + + a { + color: var(--rp-container-danger-text); + } +} + +// details +.rp-callout--details { + border-color: var(--rp-container-details-border); + background-color: var(--rp-container-details-bg); + + .rp-callout__title { + color: var(--rp-container-details-text); + &::before { + background-color: var(--rp-container-details-text); + } + } + + // inline code + :not(pre, div) > code { + color: var(--rp-container-details-text); + background-color: var(--rp-container-details-code-bg); + } + + a { + color: var(--rp-container-details-text); + } +} + +details.rp-callout { + padding: 20px 24px 12px 24px; // override default padding + margin: 24px 0; // override default padding + + font-size: normal; + cursor: pointer; + transition: background-color 0.3s ease; + + &:hover { + background-color: var(--rp-c-bg-mute); + } +} diff --git a/packages/theme-default/src/components/Callout/index.tsx b/packages/theme-default/src/components/Callout/index.tsx new file mode 100644 index 000000000..1d56bd378 --- /dev/null +++ b/packages/theme-default/src/components/Callout/index.tsx @@ -0,0 +1,47 @@ +import type { ReactNode } from 'react'; +import './index.scss'; + +export interface CalloutProps { + type: 'tip' | 'note' | 'warning' | 'caution' | 'danger' | 'info' | 'details'; + title?: string; + children: ReactNode; +} + +/** + * Construct the DOM structure of the container directive. + * For example: + * + * ::: tip {title="foo"} + * This is a tip + * ::: + * + * will be transformed to: + * + *
    + *
    Tip
    + *
    + *

    This is a tip

    + *
    + *
    + */ +export function Callout({ type, title, children }: CalloutProps): ReactNode { + const isDetails = type === 'details'; + + if (isDetails) { + return ( +
    + {title} +
    {children}
    +
    + ); + } + + return ( +
    +
    {title}
    +
    {children}
    +
    + ); +} + +export default Callout; diff --git a/packages/theme-default/src/components/CodeBlockRuntime/index.tsx b/packages/theme-default/src/components/CodeBlockRuntime/index.tsx index 80303dc9b..4b86d33dd 100644 --- a/packages/theme-default/src/components/CodeBlockRuntime/index.tsx +++ b/packages/theme-default/src/components/CodeBlockRuntime/index.tsx @@ -9,11 +9,11 @@ import { codeToHast, createCssVariablesTheme, } from 'shiki'; -import { Code } from '../../layout/DocLayout/docComponents/code'; +import { Code } from '../DocContent/docComponents/codeblock/code'; import { PreWithCodeButtonGroup, type PreWithCodeButtonGroupProps, -} from '../../layout/DocLayout/docComponents/pre'; +} from '../DocContent/docComponents/codeblock/pre'; export interface CodeBlockRuntimeProps extends PreWithCodeButtonGroupProps { lang: string; diff --git a/packages/theme-default/src/components/DocContent/FallbackHeading.tsx b/packages/theme-default/src/components/DocContent/FallbackHeading.tsx new file mode 100644 index 000000000..80002d2b7 --- /dev/null +++ b/packages/theme-default/src/components/DocContent/FallbackHeading.tsx @@ -0,0 +1,43 @@ +import { slug } from 'github-slugger'; +import { renderInlineMarkdown } from '../../logic/utils'; +import { A } from './docComponents/a'; +import { H1, H2, H3, H4, H5, H6 } from './docComponents/title'; + +const HEADING_MAP = { + 1: H1, + 2: H2, + 3: H3, + 4: H4, + 5: H5, + 6: H6, +}; + +/** + * Escape Hatch + * A fallback heading component in runtime that generates an anchor link based on the title prop. + * + * @param level - The heading level (1-6). + * @param title - The title text to display in the heading. + */ +export function FallbackHeading({ + level, + title, +}: { + level: 1 | 2 | 3 | 4 | 5 | 6; + title: string; +}) { + const titleSlug = title && slug(title.trim()); + + const Element = HEADING_MAP[level] || H1; + + return ( + titleSlug && ( + + + + # + + + ) + ); +} diff --git a/packages/theme-default/src/styles/doc.scss b/packages/theme-default/src/components/DocContent/doc.scss similarity index 75% rename from packages/theme-default/src/styles/doc.scss rename to packages/theme-default/src/components/DocContent/doc.scss index 17a137979..b336d6103 100644 --- a/packages/theme-default/src/styles/doc.scss +++ b/packages/theme-default/src/components/DocContent/doc.scss @@ -27,7 +27,7 @@ /* link */ /* #region */ // we do not style all the links, only the links with class "rp-link" because there are too many usages of tag in user custom components - &:where(a.rp-link):not(:where(.header-anchor)) { + &:where(a.rp-link):not(:where(.rp-header-anchor)) { font-weight: 500; color: var(--rp-c-link); transition: color 0.25s; @@ -63,9 +63,8 @@ /* #region */ &:where(h1, h2, h3, h4, h5, h6) { font-weight: 600; - position: relative; outline: none; - & .header-anchor { + & .rp-header-anchor { float: left; margin-left: -0.8em; font-weight: 500; @@ -74,17 +73,21 @@ transition: color 0.25s, opacity 0.25s; + &:hover { + border-bottom: 1px solid var(--rp-c-brand); + opacity: 0.85; + } } &:hover { - & .header-anchor { + & .rp-header-anchor { opacity: 1; } - & .header-anchor:focus { + & .rp-header-anchor:focus { opacity: 1; } } } - &:where(.header-anchor) { + &:where(.rp-header-anchor) { color: var(--rp-c-brand); } &:where(h1) { @@ -163,46 +166,50 @@ /* table */ /* #region */ &:where(table) { - display: block; - border-collapse: collapse; - font-size: 1rem; - margin-top: 1.25rem; - margin-bottom: 1.25rem; - overflow-x: auto; - line-height: 1.75rem; - border: 1px solid var(--rp-c-border); + width: 100%; + border-radius: var(--rp-radius); + overflow: hidden; + box-shadow: inset 0 0 0 0.5px var(--rp-c-divider-light); + outline: 1px solid var(--rp-c-divider-light); } - &:where(table tr) { - border: 1px solid; - transition-property: color, background-color, border-color; - transition-duration: 500ms; - border-color: var(--rp-c-gray-light-3); + /* thead */ + &:where(thead) { + color: var(--rp-c-text-0); + background: var(--rp-c-bg-soft); } - &:where(table tr):nth-child(even) { - background-color: var(--rp-c-bg-soft); + &:where(th) { + padding: 1rem 2rem; + font-weight: 600; + font-size: 0.875rem; + text-align: left; + border-bottom: 1px solid var(--rp-c-divider-light); + position: relative; } - &:where(table td) { - border: 1px solid; - padding-left: 1rem; - padding-right: 1rem; - padding-top: 0.5rem; - padding-bottom: 0.5rem; - border-color: var(--rp-c-gray-light-3); + /* tbody */ + &:where(tbody) { + background: transparent; } - &:where(table th) { - border: 1px solid; - padding-left: 1rem; - padding-right: 1rem; - padding-top: 0.5rem; - padding-bottom: 0.5rem; - color: var(--rp-c-text-1); - font-size: 1rem; - font-weight: 600; - border-color: var(--rp-c-gray-light-3); + &:where(td) { + padding: 1rem 1.5rem; + border-bottom: 1px solid var(--rp-c-divider-light); + + font-size: 0.875rem; + } + + &:where(tr) { + transition: background-color 0.2s ease; + + &:hover { + background: var(--rp-c-bg-soft); + } + } + + &:where(tr:last-child td) { + border-bottom: none; } /* #endregion */ @@ -213,7 +220,11 @@ &:where(:not(pre, h1, h2, h3, h4, h5, h6) > code) { font-size: var(--rp-code-font-size); } + &:where(h1 code, h2 code, h3 code) { + font-size: 0.9em; + } &:where(:not(pre, div) > code) { + font-family: var(--rp-font-family-mono); padding: 3px 6px; border-radius: var(--rp-radius-small); background-color: var(--rp-c-text-code-bg); @@ -222,9 +233,6 @@ &:where(:not(pre, div, a) > code) { color: var(--rp-c-text-code); } - &:where(h1 code, h2 code, h3 code) { - font-size: 0.9em; - } &:where(a > code) { color: var(--rp-c-brand-dark); transition: color 0.25s; @@ -237,9 +245,10 @@ &:where(div[class^='language-']) { position: relative; margin: 16px 0; - background-color: var(--rp-code-block-bg); + border: var(--rp-code-block-border); overflow-x: auto; transition: none; + @media (min-width: 641px) { border-radius: var(--rp-radius); } @@ -266,11 +275,24 @@ } & :where(pre) { + outline: none; // avoid focus outline https://github.com/web-infra-dev/rspack/issues/11775 + position: relative; z-index: 1; margin: 0; background: transparent; overflow-x: auto; + &::-webkit-scrollbar { + height: 5px; + width: 5px; + } + &::-webkit-scrollbar-corner { + display: none; + } + &::-webkit-scrollbar-thumb { + background-color: var(--rp-code-block-scroll-bar-bg); + border-radius: 10px; + } } & :where(code) { @@ -279,28 +301,38 @@ width: fit-content; min-width: 100%; line-height: 1.7; - font-size: var(--rp-code-font-size); - color: var(--rp-code-block-color); - transition: color 0.5s; } } &:where(.rspress-code-title) { + font-family: var(--rp-font-family-mono); padding: 12px 16px; font-size: var(--rp-code-font-size); background-color: var(--rp-code-title-bg); - transition: background-color 0.5s; } &:where(.rspress-code-content) { + font-size: var(--rp-code-font-size); + font-family: var(--rp-font-family-mono); position: relative; + color: var(--rp-code-block-color); + background-color: var(--rp-code-block-bg); } /* #endregion */ + + &:where(details) { + cursor: pointer; + margin: 16px 0; + padding: 8px; + transition: background-color 0.3s; + &:hover { + background-color: var(--rp-c-bg-mute); + } + } } } /* Dark mode styles */ -:where(.rp-dark) .rspress-doc, :where(.rp-dark) .rp-doc { :not(:where(.rp-not-doc, .rp-not-doc *)) { /* table */ diff --git a/packages/theme-default/src/layout/DocLayout/docComponents/a.tsx b/packages/theme-default/src/components/DocContent/docComponents/a.tsx similarity index 100% rename from packages/theme-default/src/layout/DocLayout/docComponents/a.tsx rename to packages/theme-default/src/components/DocContent/docComponents/a.tsx diff --git a/packages/theme-default/src/layout/DocLayout/docComponents/code/index.module.scss b/packages/theme-default/src/components/DocContent/docComponents/codeblock/CodeButtonGroup.module.scss similarity index 71% rename from packages/theme-default/src/layout/DocLayout/docComponents/code/index.module.scss rename to packages/theme-default/src/components/DocContent/docComponents/codeblock/CodeButtonGroup.module.scss index cca611882..b6c0059d3 100644 --- a/packages/theme-default/src/layout/DocLayout/docComponents/code/index.module.scss +++ b/packages/theme-default/src/components/DocContent/docComponents/codeblock/CodeButtonGroup.module.scss @@ -37,30 +37,6 @@ } } -.code-copy-button { - .icon-success { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%) scale(0); - opacity: 0; - color: #00d600; - } -} - -.code-copied { - .icon-copy { - transform: scale(0.33); - opacity: 0; - } - - .icon-success { - transform: translate(-50%, -50%) scale(1); - opacity: 1; - transition-delay: 0.075s; - } -} - .icon-wrapped { position: absolute; top: 50%; diff --git a/packages/theme-default/src/layout/DocLayout/docComponents/code/CodeButtonGroup.tsx b/packages/theme-default/src/components/DocContent/docComponents/codeblock/CodeButtonGroup.tsx similarity index 85% rename from packages/theme-default/src/layout/DocLayout/docComponents/code/CodeButtonGroup.tsx rename to packages/theme-default/src/components/DocContent/docComponents/codeblock/CodeButtonGroup.tsx index fd7fe11cf..d64fe94e3 100644 --- a/packages/theme-default/src/layout/DocLayout/docComponents/code/CodeButtonGroup.tsx +++ b/packages/theme-default/src/components/DocContent/docComponents/codeblock/CodeButtonGroup.tsx @@ -2,12 +2,11 @@ import { useSite } from '@rspress/runtime'; import IconWrap from '@theme-assets/wrap'; import IconWrapped from '@theme-assets/wrapped'; import { useState } from 'react'; -import { SvgWrapper } from '../../../../components/SvgWrapper'; +import { SvgWrapper } from '../../../SvgWrapper'; +import * as styles from './CodeButtonGroup.module.scss'; import { CopyCodeButton } from './CopyCodeButton'; -import * as styles from './index.module.scss'; -export interface CodeButtonGroupProps - extends ReturnType { +export interface CodeButtonGroupProps extends ReturnType { preElementRef: React.RefObject; /** @@ -20,7 +19,7 @@ export interface CodeButtonGroupProps showCopyButton?: boolean; } -export const useCodeButtonGroup = () => { +export const useCodeWrap = () => { const { site } = useSite(); const { defaultWrapCode } = site.markdown; const [codeWrap, setCodeWrap] = useState(defaultWrapCode); diff --git a/packages/theme-default/src/components/DocContent/docComponents/codeblock/CopyCodeButton.module.scss b/packages/theme-default/src/components/DocContent/docComponents/codeblock/CopyCodeButton.module.scss new file mode 100644 index 000000000..4cc5e9050 --- /dev/null +++ b/packages/theme-default/src/components/DocContent/docComponents/codeblock/CopyCodeButton.module.scss @@ -0,0 +1,23 @@ +.code-copy-button { + .icon-success { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%) scale(0); + opacity: 0; + color: #00d600; + } +} + +.code-copied { + .icon-copy { + transform: scale(0.33); + opacity: 0; + } + + .icon-success { + transform: translate(-50%, -50%) scale(1); + opacity: 1; + transition-delay: 0.075s; + } +} diff --git a/packages/theme-default/src/layout/DocLayout/docComponents/code/CopyCodeButton.tsx b/packages/theme-default/src/components/DocContent/docComponents/codeblock/CopyCodeButton.tsx similarity index 93% rename from packages/theme-default/src/layout/DocLayout/docComponents/code/CopyCodeButton.tsx rename to packages/theme-default/src/components/DocContent/docComponents/codeblock/CopyCodeButton.tsx index 5c86bd2a9..1b78196d7 100644 --- a/packages/theme-default/src/layout/DocLayout/docComponents/code/CopyCodeButton.tsx +++ b/packages/theme-default/src/components/DocContent/docComponents/codeblock/CopyCodeButton.tsx @@ -2,8 +2,8 @@ import IconCopy from '@theme-assets/copy'; import IconSuccess from '@theme-assets/success'; import copy from 'copy-to-clipboard'; import { useRef } from 'react'; -import { SvgWrapper } from '../../../../components/SvgWrapper'; -import * as styles from './index.module.scss'; +import { SvgWrapper } from '../../../SvgWrapper'; +import * as styles from './CopyCodeButton.module.scss'; const timeoutIdMap: Map = new Map(); diff --git a/packages/theme-default/src/layout/DocLayout/docComponents/code/index.tsx b/packages/theme-default/src/components/DocContent/docComponents/codeblock/code.tsx similarity index 100% rename from packages/theme-default/src/layout/DocLayout/docComponents/code/index.tsx rename to packages/theme-default/src/components/DocContent/docComponents/codeblock/code.tsx diff --git a/packages/theme-default/src/components/DocContent/docComponents/codeblock/pre.module.scss b/packages/theme-default/src/components/DocContent/docComponents/codeblock/pre.module.scss new file mode 100644 index 000000000..a2d420aef --- /dev/null +++ b/packages/theme-default/src/components/DocContent/docComponents/codeblock/pre.module.scss @@ -0,0 +1,5 @@ +.force-wrap { + code { + white-space: pre-wrap !important; + } +} diff --git a/packages/theme-default/src/layout/DocLayout/docComponents/pre.tsx b/packages/theme-default/src/components/DocContent/docComponents/codeblock/pre.tsx similarity index 82% rename from packages/theme-default/src/layout/DocLayout/docComponents/pre.tsx rename to packages/theme-default/src/components/DocContent/docComponents/codeblock/pre.tsx index 450867637..ca61c6ac8 100644 --- a/packages/theme-default/src/layout/DocLayout/docComponents/pre.tsx +++ b/packages/theme-default/src/components/DocContent/docComponents/codeblock/pre.tsx @@ -1,9 +1,11 @@ +import clsx from 'clsx'; import { isValidElement, useRef } from 'react'; import { CodeButtonGroup, type CodeButtonGroupProps, - useCodeButtonGroup, -} from './code/CodeButtonGroup'; + useCodeWrap, +} from './CodeButtonGroup'; +import { forceWrap } from './pre.module.scss'; export type ShikiPreProps = { containerElementClassName: string | undefined; @@ -28,17 +30,22 @@ function ShikiPre({ codeButtonGroupProps, ...otherProps }: ShikiPreProps) { - const { codeWrap, toggleCodeWrap } = useCodeButtonGroup(); + const { codeWrap, toggleCodeWrap } = useCodeWrap(); return (
    - {title &&
    {title}
    } -
    + {title && ( +
    {title}
    + )} +
                 {child}
    @@ -70,8 +77,8 @@ export interface PreWithCodeButtonGroupProps
      * expected wrapped pre element is:
      * ```html
      *
    - *
    test.js
    - *
    + *
    test.js
    + *
    *
    *
      *        
    diff --git a/packages/theme-default/src/layout/DocLayout/docComponents/hr.tsx b/packages/theme-default/src/components/DocContent/docComponents/hr.tsx
    similarity index 100%
    rename from packages/theme-default/src/layout/DocLayout/docComponents/hr.tsx
    rename to packages/theme-default/src/components/DocContent/docComponents/hr.tsx
    diff --git a/packages/theme-default/src/layout/DocLayout/docComponents/img.tsx b/packages/theme-default/src/components/DocContent/docComponents/img.tsx
    similarity index 100%
    rename from packages/theme-default/src/layout/DocLayout/docComponents/img.tsx
    rename to packages/theme-default/src/components/DocContent/docComponents/img.tsx
    diff --git a/packages/theme-default/src/layout/DocLayout/docComponents/index.tsx b/packages/theme-default/src/components/DocContent/docComponents/index.tsx
    similarity index 86%
    rename from packages/theme-default/src/layout/DocLayout/docComponents/index.tsx
    rename to packages/theme-default/src/components/DocContent/docComponents/index.tsx
    index 82f8bcf23..03c8ce5c3 100644
    --- a/packages/theme-default/src/layout/DocLayout/docComponents/index.tsx
    +++ b/packages/theme-default/src/components/DocContent/docComponents/index.tsx
    @@ -1,10 +1,10 @@
     import { A } from './a';
    -import { Code } from './code';
    +import { Code } from './codeblock/code';
    +import { PreWithCodeButtonGroup } from './codeblock/pre';
     import { Hr } from './hr';
     import { Img } from './img';
     import { Li, Ol, Ul } from './list';
     import { Blockquote, P, Strong } from './paragraph';
    -import { PreWithCodeButtonGroup } from './pre';
     import { Table, Td, Th, Tr } from './table';
     import { H1, H2, H3, H4, H5, H6 } from './title';
     
    diff --git a/packages/theme-default/src/layout/DocLayout/docComponents/list.tsx b/packages/theme-default/src/components/DocContent/docComponents/list.tsx
    similarity index 100%
    rename from packages/theme-default/src/layout/DocLayout/docComponents/list.tsx
    rename to packages/theme-default/src/components/DocContent/docComponents/list.tsx
    diff --git a/packages/theme-default/src/layout/DocLayout/docComponents/paragraph.tsx b/packages/theme-default/src/components/DocContent/docComponents/paragraph.tsx
    similarity index 100%
    rename from packages/theme-default/src/layout/DocLayout/docComponents/paragraph.tsx
    rename to packages/theme-default/src/components/DocContent/docComponents/paragraph.tsx
    diff --git a/packages/theme-default/src/layout/DocLayout/docComponents/table.tsx b/packages/theme-default/src/components/DocContent/docComponents/table.tsx
    similarity index 100%
    rename from packages/theme-default/src/layout/DocLayout/docComponents/table.tsx
    rename to packages/theme-default/src/components/DocContent/docComponents/table.tsx
    diff --git a/packages/theme-default/src/layout/DocLayout/docComponents/title.tsx b/packages/theme-default/src/components/DocContent/docComponents/title.tsx
    similarity index 100%
    rename from packages/theme-default/src/layout/DocLayout/docComponents/title.tsx
    rename to packages/theme-default/src/components/DocContent/docComponents/title.tsx
    diff --git a/packages/theme-default/src/components/DocContent/index.tsx b/packages/theme-default/src/components/DocContent/index.tsx
    new file mode 100644
    index 000000000..50c3e2c5f
    --- /dev/null
    +++ b/packages/theme-default/src/components/DocContent/index.tsx
    @@ -0,0 +1,39 @@
    +import { MDXProvider } from '@mdx-js/react';
    +import { Content, usePage, useSite } from '@rspress/runtime';
    +import { Callout, getCustomMDXComponent } from '@theme';
    +import './doc.scss';
    +import { FallbackHeading } from './FallbackHeading';
    +
    +export function FallbackTitle() {
    +  const { site } = useSite();
    +  const { page } = usePage();
    +  const { headingTitle, title } = page;
    +
    +  return (
    +    site.themeConfig.fallbackHeadingTitle !== false &&
    +    !headingTitle && 
    +  );
    +}
    +
    +export function DocContent({
    +  components,
    +  isOverviewPage = false,
    +}: {
    +  components: Record> | undefined;
    +  isOverviewPage?: boolean;
    +}) {
    +  const mdxComponents = {
    +    ...getCustomMDXComponent(),
    +    ...components,
    +
    +    // custom components can be added here
    +    $$$callout$$$: Callout, // FIXME: For .md files, .md files cannot add import statements
    +  };
    +
    +  return (
    +    
    +      {!isOverviewPage && }
    +      
    +    
    +  );
    +}
    diff --git a/packages/theme-default/src/components/DocFooter/index.module.scss b/packages/theme-default/src/components/DocFooter/index.module.scss
    index 7bc811337..7c72b354a 100644
    --- a/packages/theme-default/src/components/DocFooter/index.module.scss
    +++ b/packages/theme-default/src/components/DocFooter/index.module.scss
    @@ -1,20 +1,9 @@
    -@media (min-width: 640px) {
    -  .pager {
    -    display: flex;
    -    flex-direction: column;
    -    width: 50%;
    -  }
    -
    -  .pager.has-next {
    -    padding-top: 0;
    -    padding-left: 16px;
    -  }
    +.docFooter {
    +  margin-top: 48px;
     }
    -
    -.prev {
    -  width: 100%;
    -}
    -
    -.next {
    +.divider {
       width: 100%;
    +  height: 0.5px;
    +  background-color: var(--rp-c-divider-light);
    +  margin: 48px 0;
     }
    diff --git a/packages/theme-default/src/components/DocFooter/index.tsx b/packages/theme-default/src/components/DocFooter/index.tsx
    index 3e188ee6d..8fb24ee40 100644
    --- a/packages/theme-default/src/components/DocFooter/index.tsx
    +++ b/packages/theme-default/src/components/DocFooter/index.tsx
    @@ -1,43 +1,18 @@
     import { useLocaleSiteData, useSite } from '@rspress/runtime';
    -import { EditLink, LastUpdated, PrevNextPage } from '@theme';
    -import { usePrevNextPage } from '../../logic/usePrevNextPage';
    +import { LastUpdated, PrevNextPage } from '@theme';
     import * as styles from './index.module.scss';
     
     export function DocFooter() {
    -  const { prevPage, nextPage } = usePrevNextPage();
       const { lastUpdated: localesLastUpdated = false } = useLocaleSiteData();
       const { site } = useSite();
       const { themeConfig } = site;
       const showLastUpdated = themeConfig.lastUpdated || localesLastUpdated;
     
       return (
    -