Files
docsify/test/e2e/search.test.js

755 lines
22 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import docsifyInit from '../helpers/docsify-init.js';
import { test, expect } from './fixtures/docsify-init-fixture.js';
test.describe('Search Plugin Tests', () => {
test('search readme', async ({ page }) => {
const docsifyInitConfig = {
markdown: {
homepage: `
# Hello World
This is the homepage.
`,
sidebar: `
- [Test Page](test)
`,
},
routes: {
'/test.md': `
# Test Page
This is a custom route.
`,
},
scriptURLs: ['/dist/plugins/search.js'],
};
const searchFieldElm = page.locator('input[type=search]');
const resultsHeadingElm = page.locator('.results-panel .title');
await docsifyInit(docsifyInitConfig);
await searchFieldElm.fill('hello');
await expect(resultsHeadingElm).toHaveText('Hello World');
await page.click('.clear-button');
await searchFieldElm.fill('test');
await expect(resultsHeadingElm).toHaveText('Test Page');
});
test.describe('resultSource option', () => {
const markdown = {
homepage: `
# Hello World
This is the homepage.
`,
sidebar: `
- Guides 📚
- [Test Page 🚀](test)
`,
};
const routes = {
'/test.md': `
# Test Page 🧪
This is a custom route.
## Deep Section
Content about volcanoes.
`,
};
test('shows nothing by default', async ({ page }) => {
const docsifyInitConfig = {
markdown,
routes,
scriptURLs: ['/dist/plugins/search.js'],
};
const searchFieldElm = page.locator('input[type=search]');
const resultsHeadingElm = page.locator('.results-panel .title');
const resultsPageElm = page.locator('.results-panel .search-breadcrumb');
await docsifyInit(docsifyInitConfig);
await searchFieldElm.fill('volcanoes');
await expect(resultsHeadingElm).toHaveText('Deep Section');
await expect(resultsPageElm).toHaveCount(0);
});
test('page mode shows page title for section matches', async ({ page }) => {
const docsifyInitConfig = {
config: {
search: {
resultSource: 'page',
},
},
markdown,
routes,
scriptURLs: ['/dist/plugins/search.js'],
};
const searchFieldElm = page.locator('input[type=search]');
const resultsHeadingElm = page.locator('.results-panel .title');
const resultsPageElm = page.locator('.results-panel .search-breadcrumb');
await docsifyInit(docsifyInitConfig);
// A match inside a section shows which page it comes from,
// with emoji stripped from the page title.
await searchFieldElm.fill('volcanoes');
await expect(resultsHeadingElm).toHaveText('Deep Section');
await expect(resultsPageElm).toHaveText('Test Page');
// A match on the page title itself does not repeat the page title.
await page.click('.clear-button');
await searchFieldElm.fill('custom route');
await expect(resultsHeadingElm).toHaveText('Test Page 🧪');
await expect(resultsPageElm).toHaveCount(0);
});
test('breadcrumb mode shows sidebar path to the page', async ({ page }) => {
const docsifyInitConfig = {
config: {
search: {
resultSource: 'breadcrumb',
},
},
markdown,
routes,
scriptURLs: ['/dist/plugins/search.js'],
};
const searchFieldElm = page.locator('input[type=search]');
const resultsHeadingElm = page.locator('.results-panel .title');
const resultsPageElm = page.locator('.results-panel .search-breadcrumb');
await docsifyInit(docsifyInitConfig);
// Emoji from sidebar labels are stripped from the breadcrumb.
await searchFieldElm.fill('volcanoes');
await expect(resultsHeadingElm).toHaveText('Deep Section');
await expect(resultsPageElm).toHaveText('Guides › Test Page');
// A page absent from the sidebar falls back to its page title.
await page.click('.clear-button');
await searchFieldElm.fill('homepage');
await expect(resultsHeadingElm).toHaveText('Hello World');
await expect(resultsPageElm).toHaveText('Hello World');
});
});
test('updates the active sidebar anchor after clicking a heading result', async ({
page,
}) => {
const docsifyInitConfig = {
config: {
subMaxLevel: 2,
search: {
paths: ['/'],
},
},
markdown: {
homepage: `
# Home
## Installation
Searchable installation content.
`,
sidebar: '- [Home](/)',
},
scriptURLs: ['/dist/plugins/search.js'],
};
const searchFieldElm = page.locator('input[type=search]');
const resultsHeadingElm = page.locator('.results-panel .title');
const installationSidebarItem = page
.locator('.sidebar-nav a[href="#/?id=installation"]')
.locator('..');
await docsifyInit(docsifyInitConfig);
await searchFieldElm.fill('installation');
await expect(resultsHeadingElm).toHaveText('Installation');
await page.click('.matching-post a');
await expect(installationSidebarItem).toHaveClass(/active/);
});
test('search ignore title', async ({ page }) => {
const docsifyInitConfig = {
markdown: {
homepage: `
# Hello World
This is the homepage.
`,
sidebar: `
- [Home page](/)
- [GitHub Pages](github)
`,
},
routes: {
'/github.md': `
# GitHub Pages
This is the GitHub Pages.
## GitHub Pages ignore1 <!-- {docsify-ignore} -->
There're three places to populate your docs for your GitHub repository1.
## GitHub Pages ignore2 {docsify-ignore}
There're three places to populate your docs for your GitHub repository2.
`,
},
scriptURLs: ['/dist/plugins/search.js'],
};
const searchFieldElm = page.locator('input[type=search]');
const resultsHeadingElm = page.locator('.results-panel .title');
await docsifyInit(docsifyInitConfig);
await searchFieldElm.fill('repository1');
await expect(resultsHeadingElm).toHaveText('GitHub Pages ignore1');
await page.click('.clear-button');
await searchFieldElm.fill('repository2');
await expect(resultsHeadingElm).toHaveText('GitHub Pages ignore2');
});
test('search only one homepage', async ({ page }) => {
const docsifyInitConfig = {
markdown: {
sidebar: `
- [README](README)
- [Test Page](test)
`,
},
routes: {
'/README.md': `
# Hello World
This is the homepage.
`,
'/test.md': `
# Test Page
This is a custom route.
`,
},
scriptURLs: ['/dist/plugins/search.js'],
};
const searchFieldElm = page.locator('input[type=search]');
const resultsHeadingElm = page.locator('.results-panel .title');
const resultElm = page.locator('.matching-post');
await docsifyInit(docsifyInitConfig);
await searchFieldElm.fill('hello');
await expect(resultElm).toHaveCount(1);
await expect(resultsHeadingElm).toHaveText('Hello World');
await page.click('.clear-button');
await searchFieldElm.fill('test');
await expect(resultsHeadingElm).toHaveText('Test Page');
});
test('search ignore diacritical marks', async ({ page }) => {
const docsifyInitConfig = {
markdown: {
homepage: `
# Qué es
docsify genera su sitio web de documentación sobre la marcha. A diferencia de GitBook, no genera archivos estáticos html. En cambio, carga y analiza de forma inteligente sus archivos de Markdown y los muestra como sitio web. Todo lo que necesita hacer es crear un index.html para comenzar y desplegarlo en GitHub Pages.
`,
},
scriptURLs: ['/dist/plugins/search.js'],
};
const searchFieldElm = page.locator('input[type=search]');
const resultsHeadingElm = page.locator('.results-panel .title');
await docsifyInit(docsifyInitConfig);
await searchFieldElm.fill('documentacion');
await expect(resultsHeadingElm).toHaveText('Que es');
await page.click('.clear-button');
await searchFieldElm.fill('estáticos');
await expect(resultsHeadingElm).toHaveText('Que es');
});
test('search when there is no title', async ({ page }) => {
const docsifyInitConfig = {
markdown: {
homepage: `
This is some description. We assume autoHeader added the # Title. A long paragraph.
`,
sidebar: `
- [Changelog](changelog)
`,
},
routes: {
'/changelog.md': `
feat: Support search when there is no title
## Changelog Title
hello, this is a changelog
`,
},
scriptURLs: ['/dist/plugins/search.js'],
};
const searchFieldElm = page.locator('input[type=search]');
const resultsHeadingElm = page.locator('.results-panel .title');
await docsifyInit(docsifyInitConfig);
await searchFieldElm.fill('paragraph');
await expect(resultsHeadingElm).toHaveText('Home Page');
await page.click('.clear-button');
await searchFieldElm.fill('Support');
await expect(resultsHeadingElm).toHaveText('changelog');
await page.click('.clear-button');
await searchFieldElm.fill('hello');
await expect(resultsHeadingElm).toHaveText('Changelog Title');
});
test('search when there is no body', async ({ page }) => {
const docsifyInitConfig = {
markdown: {
homepage: `
# EmptyContent
---
---
`,
},
scriptURLs: ['/dist/plugins/search.js'],
};
const searchFieldElm = page.locator('input[type=search]');
const resultsHeadingElm = page.locator('.results-panel .title');
await docsifyInit(docsifyInitConfig);
await searchFieldElm.fill('empty');
await expect(resultsHeadingElm).toHaveText('EmptyContent');
});
test('does not restore deleted sections after rebuilding an expired cache', async ({
page,
}) => {
const options = {
config: {
search: { paths: ['/'], namespace: 'cache-refresh', maxAge: -1 },
},
markdown: { homepage: '# Obsolete\n\nretiredkeyword' },
scriptURLs: ['/dist/plugins/search.js'],
};
const searchField = page.locator('input[type=search]');
const results = page.locator('.results-panel .title');
await docsifyInit(options);
await searchField.fill('retiredkeyword');
await expect(results).toHaveText('Obsolete');
options.config.search.maxAge = 60000;
options.markdown.homepage = '# Current\n\ncurrentkeyword';
await docsifyInit(options);
await searchField.fill('currentkeyword');
await expect(results).toHaveText('Current');
await searchField.fill('retiredkeyword');
await expect(page.getByText('No Results!', { exact: true })).toBeVisible();
await docsifyInit(options);
await searchField.fill('currentkeyword');
await expect(results).toHaveText('Current');
await searchField.fill('retiredkeyword');
await expect(page.getByText('No Results!', { exact: true })).toBeVisible();
});
test('preserves another namespace when saving a search index', async ({
page,
}) => {
const otherSite = {
config: { search: { paths: ['/'], namespace: 'other-site' } },
markdown: { homepage: '# Other site\n\nprotectedkeyword' },
scriptURLs: ['/dist/plugins/search.js'],
};
const searchField = page.locator('input[type=search]');
const results = page.locator('.results-panel .title');
await docsifyInit(otherSite);
await searchField.fill('protectedkeyword');
await expect(results).toHaveText('Other site');
await docsifyInit({
config: { search: { paths: ['/'], namespace: 'current-site' } },
markdown: { homepage: '# Current site\n\ncurrentkeyword' },
scriptURLs: ['/dist/plugins/search.js'],
});
await searchField.fill('currentkeyword');
await expect(results).toHaveText('Current site');
otherSite.markdown.homepage = '# Changed\n\nchangedkeyword';
await docsifyInit(otherSite);
await searchField.fill('protectedkeyword');
await expect(results).toHaveText('Other site');
});
test('keeps saving index when one auto path request fails with cached records', async ({
page,
}) => {
const indexKey = 'docsify.search.index';
const expireKey = 'docsify.search.expires';
const pageErrors = [];
page.on('pageerror', error => pageErrors.push(error.message));
await page.evaluate(
({ indexKey, expireKey }) => {
return new Promise((resolve, reject) => {
const request = indexedDB.open('docsify', 1);
request.onupgradeneeded = () => {
const db = request.result;
if (!db.objectStoreNames.contains('search')) {
db.createObjectStore('search', { keyPath: 'slug' });
}
if (!db.objectStoreNames.contains('expires')) {
db.createObjectStore('expires', { keyPath: 'key' });
}
};
request.onerror = () => reject(request.error);
request.onsuccess = () => {
const db = request.result;
const tx = db.transaction(['search', 'expires'], 'readwrite');
tx.objectStore('search').put({
slug: '/cached',
title: 'Cached Page',
body: 'cached record',
path: '/cached',
indexKey,
});
tx.objectStore('expires').put({
key: expireKey,
value: Date.now() + 60 * 1000,
});
tx.oncomplete = () => {
db.close();
resolve();
};
tx.onerror = () => reject(tx.error);
};
});
},
{ indexKey, expireKey },
);
await docsifyInit({
markdown: {
homepage: '# Home',
sidebar: `
- [Cached](cached)
- [Success](success)
- [Fail](fail)
`,
},
routes: {
'/success.md': '# Success\n\nregressionKeyword',
'/fail.md': {
status: 404,
body: 'Not Found',
contentType: 'text/markdown',
},
},
scriptURLs: ['/dist/plugins/search.js'],
});
await expect
.poll(async () => {
return await page.evaluate(indexKey => {
return new Promise((resolve, reject) => {
const request = indexedDB.open('docsify');
request.onerror = () => reject(request.error);
request.onsuccess = () => {
const db = request.result;
const tx = db.transaction(['search', 'expires'], 'readonly');
const searchStore = tx.objectStore('search');
const expiresStore = tx.objectStore('expires');
const searchReq = searchStore.getAll();
const expiresReq = expiresStore.get('docsify.search.expires');
tx.onerror = () => reject(tx.error);
tx.oncomplete = () => {
const records = Array.isArray(searchReq.result)
? searchReq.result
: [];
const hasSuccessRecord = records.some(
record =>
record &&
record.indexKey === indexKey &&
record.path === '/success',
);
const hasInvalidRecord = records.some(
record => !record || typeof record.slug !== 'string',
);
const hasExpireRecord = Boolean(expiresReq.result?.value);
db.close();
resolve(
hasSuccessRecord && hasExpireRecord && !hasInvalidRecord,
);
};
};
});
}, indexKey);
})
.toBe(true);
const searchFieldElm = page.locator('input[type=search]');
const resultsHeadingElm = page.locator('.results-panel .title');
await searchFieldElm.fill('regressionKeyword');
await expect(resultsHeadingElm).toHaveText('Success');
expect(pageErrors).toEqual([]);
});
test('handles default focusSearch binding', async ({ page }) => {
const docsifyInitConfig = {
scriptURLs: ['/dist/plugins/search.js'],
};
const searchFieldElm = page.locator('input[type="search"]');
await docsifyInit(docsifyInitConfig);
await expect(searchFieldElm).not.toBeFocused();
await page.keyboard.press('/');
await expect(searchFieldElm).toBeFocused();
});
test('handles custom focusSearch binding', async ({ page }) => {
const docsifyInitConfig = {
config: {
search: {
keyBindings: ['z'],
},
},
scriptURLs: ['/dist/plugins/search.js'],
};
const searchFieldElm = page.locator('input[type="search"]');
await docsifyInit(docsifyInitConfig);
await expect(searchFieldElm).not.toBeFocused();
await page.keyboard.press('/');
await expect(searchFieldElm).not.toBeFocused();
await page.keyboard.press('z');
await expect(searchFieldElm).toBeFocused();
});
test('search result should remove markdown code block', async ({ page }) => {
const docsifyInitConfig = {
markdown: {
homepage: `
# Hello World
searchHere
\`\`\`js
console.log('Hello World');
\`\`\`
`,
},
scriptURLs: ['/dist/plugins/search.js'],
};
const searchFieldElm = page.locator('input[type=search]');
const resultsHeadingElm = page.locator('.results-panel .content');
await docsifyInit(docsifyInitConfig);
await searchFieldElm.fill('searchHere');
// there is a newline after searchHere and the markdown part ```js ``` it should be removed
expect(await resultsHeadingElm.textContent()).toContain(
"...searchHere\nconsole.log('Hello World');...",
);
});
for (const [name, markdown, title] of [
[
'after a heading',
'# Code examples\n\n```html\n<customwidget />\n```',
'Code examples',
],
[
'after a paragraph',
'# Code examples\n\nAn example:\n\n```html\n<customwidget />\n```',
'Code examples',
],
['without a heading', '```html\n<customwidget />\n```', 'Home Page'],
[
'in an indented code block',
'# Code examples\n\n <customwidget />',
'Code examples',
],
]) {
test(`searches HTML examples ${name}`, async ({ page }) => {
await docsifyInit({
markdown: { homepage: markdown },
scriptURLs: ['/dist/plugins/search.js'],
});
await page.locator('input[type=search]').fill('customwidget');
await expect(page.locator('.results-panel .title')).toHaveText(title);
await expect(page.locator('.results-panel .content')).toContainText(
'<customwidget />',
);
const resultText = await page
.locator('.results-panel .content')
.textContent();
expect(resultText?.match(/<customwidget \/>/g)).toHaveLength(1);
await expect(page.locator('.results-panel customwidget')).toHaveCount(0);
});
}
test('search result should remove file markdown and keep href attribution for files', async ({
page,
}) => {
const docsifyInitConfig = {
markdown: {
homepage: `
# Hello World
![filename](_media/example.js ':include :type=code :fragment=demo')
`,
},
scriptURLs: ['/dist/plugins/search.js'],
};
const searchFieldElm = page.locator('input[type=search]');
const resultsHeadingElm = page.locator('.results-panel .content');
await docsifyInit(docsifyInitConfig);
await searchFieldElm.fill('filename');
expect(await resultsHeadingElm.textContent()).toContain(
'filename _media/example.js :include :type=code :fragment=demo',
);
});
test('search should index embedded include content', async ({ page }) => {
const docsifyInitConfig = {
markdown: {
homepage: `
# Include Search
![snippet](snippet.js ':include :type=code')
`,
},
routes: {
'/snippet.js': `
const embeddedSearchKeyword = 'ok';
`,
},
scriptURLs: ['/dist/plugins/search.js'],
};
const searchFieldElm = page.locator('input[type=search]');
const resultsHeadingElm = page.locator('.results-panel .title');
await docsifyInit(docsifyInitConfig);
await searchFieldElm.fill('embeddedSearchKeyword');
await expect(resultsHeadingElm).toHaveText('Include Search');
});
test('search should index embedded include content from relative path', async ({
page,
}) => {
const docsifyInitConfig = {
markdown: {
homepage: '# Home',
sidebar: '- [Guide Intro](guide/intro)',
},
routes: {
'/guide/intro.md': `
# Relative Include Search
![snippet](./snippets/demo.js ':include :type=code')
`,
'/guide/snippets/demo.js': `
const embeddedRelativeKeyword = 'ok';
`,
},
scriptURLs: ['/dist/plugins/search.js'],
};
const searchFieldElm = page.locator('input[type=search]');
const resultsHeadingElm = page.locator('.results-panel .title');
await docsifyInit(docsifyInitConfig);
await searchFieldElm.fill('embeddedRelativeKeyword');
await expect(resultsHeadingElm).toHaveText('Relative Include Search');
});
test('search result should remove checkbox markdown and keep related values', async ({
page,
}) => {
const docsifyInitConfig = {
markdown: {
homepage: `
# Hello World
- [ ] Task 1
- [x] SearchHere
- [ ] Task 3
`,
},
scriptURLs: ['/dist/plugins/search.js'],
};
const searchFieldElm = page.locator('input[type=search]');
const resultsHeadingElm = page.locator('.results-panel .content');
await docsifyInit(docsifyInitConfig);
await searchFieldElm.fill('SearchHere');
// remove the checkbox markdown and keep the related values
expect(await resultsHeadingElm.textContent()).toContain(
'...Task 1 SearchHere Task 3...',
);
});
test('search result should remove docsify self helper markdown and keep related values', async ({
page,
}) => {
const docsifyInitConfig = {
markdown: {
homepage: `
# Hello World
!> SearchHere to check it!
`,
},
scriptURLs: ['/dist/plugins/search.js'],
};
const searchFieldElm = page.locator('input[type=search]');
const resultsHeadingElm = page.locator('.results-panel .content');
await docsifyInit(docsifyInitConfig);
await searchFieldElm.fill('SearchHere');
// remove the helper markdown and keep the related values
expect(await resultsHeadingElm.textContent()).toContain(
'...SearchHere to check it!...',
);
});
});