diff --git a/docs/configuration.md b/docs/configuration.md
index f6748e6c1..e65ef4434 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -534,7 +534,7 @@ To disable emoji parsing of individual shorthand codes, replace `:` characters w
- Type: `Boolean` | `String` | `Object`
- Default: `false`
-Display default "404 - Not found" message:
+Display default "404 - Not Found" message:
```js
window.$docsify = {
diff --git a/src/core/config.js b/src/core/config.js
index 687dfd230..7322bd507 100644
--- a/src/core/config.js
+++ b/src/core/config.js
@@ -30,7 +30,7 @@ export default function (vm) {
nativeEmoji: false,
noCompileLinks: [],
noEmoji: false,
- notFoundPage: true,
+ notFoundPage: false,
plugins: [],
relativePath: false,
repo: '',
diff --git a/src/core/fetch/index.js b/src/core/fetch/index.js
index aa7e195d9..c4212be94 100644
--- a/src/core/fetch/index.js
+++ b/src/core/fetch/index.js
@@ -102,7 +102,8 @@ export function Fetch(Base) {
this.isHTML = /\.html$/g.test(file);
// create a handler that should be called if content was fetched successfully
- const contentFetched = (text, opt) => {
+ const contentFetched = (text, opt, response) => {
+ this.route.response = response;
this._renderMain(
text,
opt,
@@ -111,7 +112,8 @@ export function Fetch(Base) {
};
// and a handler that is called if content failed to fetch
- const contentFailedToFetch = _error => {
+ const contentFailedToFetch = (_error, response) => {
+ this.route.response = response;
this._fetchFallbackPage(path, qs, cb) || this._fetch404(file, qs, cb);
};
diff --git a/src/core/render/index.js b/src/core/render/index.js
index 2ee9755be..2566a1920 100644
--- a/src/core/render/index.js
+++ b/src/core/render/index.js
@@ -60,10 +60,6 @@ export function Render(Base) {
return isVue2 || isVue3;
};
- if (!html) {
- html = /* html */ `
404 - Not found
`;
- }
-
if ('Vue' in window) {
const mountedElms = dom
.findAll('.markdown-section > *')
@@ -310,8 +306,12 @@ export function Render(Base) {
}
_renderMain(text, opt = {}, next) {
- if (!text) {
- return this.#renderMain(text);
+ const { response } = this.route;
+
+ // Note: It is possible for the response to be undefined in environments
+ // where XMLHttpRequest has been modified or mocked
+ if (response && !response.ok && (!text || response.status !== 404)) {
+ text = `# ${response.status} - ${response.statusText}`;
}
this.callHook('beforeEach', text, result => {
diff --git a/src/core/router/history/hash.js b/src/core/router/history/hash.js
index 9853e7ecc..b84c8cdca 100644
--- a/src/core/router/history/hash.js
+++ b/src/core/router/history/hash.js
@@ -92,6 +92,7 @@ export class HashHistory extends History {
path,
file: this.getFile(path, true),
query: parseQuery(query),
+ response: {},
};
}
diff --git a/src/core/router/history/html5.js b/src/core/router/history/html5.js
index 080ba365d..49c1aac5e 100644
--- a/src/core/router/history/html5.js
+++ b/src/core/router/history/html5.js
@@ -59,6 +59,7 @@ export class HTML5History extends History {
path,
file: this.getFile(path),
query: parseQuery(query),
+ response: {},
};
}
}
diff --git a/src/core/util/ajax.js b/src/core/util/ajax.js
index f7f24b0e3..3bc2fec7e 100644
--- a/src/core/util/ajax.js
+++ b/src/core/util/ajax.js
@@ -4,10 +4,10 @@ import progressbar from '../render/progressbar.js';
import { noop } from './core.js';
/** @typedef {{updatedAt: string}} CacheOpt */
-
/** @typedef {{content: string, opt: CacheOpt}} CacheItem */
-
+/** @typedef {{ok: boolean, status: number, statusText: string}} ResponseStatus */
/** @type {Record} */
+
const cache = {};
/**
@@ -37,10 +37,16 @@ export function get(url, hasBar = false, headers = {}) {
return {
/**
- * @param {(text: string, opt: CacheOpt) => void} success
- * @param {(event: ProgressEvent) => void} error
+ * @param {(text: string, opt: CacheOpt, response: ResponseStatus) => void} success
+ * @param {(event: ProgressEvent, response: ResponseStatus) => void} error
*/
then(success, error = noop) {
+ const getResponseStatus = event => ({
+ ok: event.target.status >= 200 && event.target.status < 300,
+ status: event.target.status,
+ statusText: event.target.statusText,
+ });
+
if (hasBar) {
const id = setInterval(
_ =>
@@ -57,11 +63,15 @@ export function get(url, hasBar = false, headers = {}) {
});
}
- xhr.addEventListener('error', error);
+ xhr.addEventListener('error', event => {
+ error(event, getResponseStatus(event));
+ });
+
xhr.addEventListener('load', event => {
const target = /** @type {XMLHttpRequest} */ (event.target);
+
if (target.status >= 400) {
- error(event);
+ error(event, getResponseStatus(event));
} else {
if (typeof target.response !== 'string') {
throw new TypeError('Unsupported content type.');
@@ -74,7 +84,7 @@ export function get(url, hasBar = false, headers = {}) {
},
});
- success(result.content, result.opt);
+ success(result.content, result.opt, getResponseStatus(event));
}
});
},
diff --git a/test/e2e/configuration.test.js b/test/e2e/configuration.test.js
index b3f3a54ae..bca17331d 100644
--- a/test/e2e/configuration.test.js
+++ b/test/e2e/configuration.test.js
@@ -3,64 +3,146 @@ import docsifyInit from '../helpers/docsify-init.js';
import { test, expect } from './fixtures/docsify-init-fixture.js';
test.describe('Configuration options', () => {
- test('catchPluginErrors:true (handles uncaught errors)', async ({ page }) => {
- let consoleMsg, errorMsg;
-
- page.on('console', msg => (consoleMsg = msg.text()));
- page.on('pageerror', err => (errorMsg = err.message));
-
- await docsifyInit({
- config: {
- catchPluginErrors: true,
- plugins: [
- function (hook, vm) {
- hook.init(function () {
- fail();
- });
- hook.beforeEach(markdown => {
- return `${markdown}\n\nbeforeEach`;
- });
- },
- ],
- },
- markdown: {
- homepage: '# Hello World',
- },
- // _logHTML: true,
+ test.describe('catchPluginErrors', () => {
+ test('true (handles uncaught errors)', async ({ page }) => {
+ let consoleMsg, errorMsg;
+
+ page.on('console', msg => (consoleMsg = msg.text()));
+ page.on('pageerror', err => (errorMsg = err.message));
+
+ await docsifyInit({
+ config: {
+ catchPluginErrors: true,
+ plugins: [
+ function (hook, vm) {
+ hook.init(function () {
+ fail();
+ });
+ hook.beforeEach(markdown => {
+ return `${markdown}\n\nbeforeEach`;
+ });
+ },
+ ],
+ },
+ markdown: {
+ homepage: '# Hello World',
+ },
+ // _logHTML: true,
+ });
+
+ const mainElm = page.locator('#main');
+
+ expect(errorMsg).toBeUndefined();
+ expect(consoleMsg).toContain('Docsify plugin error');
+ await expect(mainElm).toContainText('Hello World');
+ await expect(mainElm).toContainText('beforeEach');
});
- const mainElm = page.locator('#main');
+ test('false (throws uncaught errors)', async ({ page }) => {
+ let consoleMsg, errorMsg;
+
+ page.on('console', msg => (consoleMsg = msg.text()));
+ page.on('pageerror', err => (errorMsg = err.message));
- expect(errorMsg).toBeUndefined();
- expect(consoleMsg).toContain('Docsify plugin error');
- await expect(mainElm).toContainText('Hello World');
- await expect(mainElm).toContainText('beforeEach');
+ await docsifyInit({
+ config: {
+ catchPluginErrors: false,
+ plugins: [
+ function (hook, vm) {
+ hook.ready(function () {
+ fail();
+ });
+ },
+ ],
+ },
+ markdown: {
+ homepage: '# Hello World',
+ },
+ // _logHTML: true,
+ });
+
+ expect(consoleMsg).toBeUndefined();
+ expect(errorMsg).toContain('fail');
+ });
});
- test('catchPluginErrors:false (throws uncaught errors)', async ({ page }) => {
- let consoleMsg, errorMsg;
+ test.describe('notFoundPage', () => {
+ test.describe('renders default error content', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.route('README.md', async route => {
+ await route.fulfill({
+ status: 500,
+ });
+ });
+ });
+
+ test('false', async ({ page }) => {
+ await docsifyInit({
+ config: {
+ notFoundPage: false,
+ },
+ });
- page.on('console', msg => (consoleMsg = msg.text()));
- page.on('pageerror', err => (errorMsg = err.message));
+ await expect(page.locator('#main')).toContainText('500');
+ });
- await docsifyInit({
- config: {
- catchPluginErrors: false,
- plugins: [
- function (hook, vm) {
- hook.ready(function () {
- fail();
- });
+ test('true with non-404 error', async ({ page }) => {
+ await docsifyInit({
+ config: {
+ notFoundPage: true,
+ },
+ routes: {
+ '_404.md': '',
},
- ],
- },
- markdown: {
- homepage: '# Hello World',
- },
- // _logHTML: true,
+ });
+
+ await expect(page.locator('#main')).toContainText('500');
+ });
+
+ test('string with non-404 error', async ({ page }) => {
+ await docsifyInit({
+ config: {
+ notFoundPage: '404.md',
+ },
+ routes: {
+ '404.md': '',
+ },
+ });
+
+ await expect(page.locator('#main')).toContainText('500');
+ });
});
- expect(consoleMsg).toBeUndefined();
- expect(errorMsg).toContain('fail');
+ test('true: renders _404.md page', async ({ page }) => {
+ const expectText = 'Pass';
+
+ await docsifyInit({
+ config: {
+ notFoundPage: true,
+ },
+ routes: {
+ '_404.md': expectText,
+ },
+ });
+
+ await page.evaluate(() => (window.location.hash = '#/fail'));
+ await expect(page.locator('#main')).toContainText(expectText);
+ });
+
+ test('string: renders specified 404 page', async ({ page }) => {
+ const expectText = 'Pass';
+
+ await docsifyInit({
+ config: {
+ notFoundPage: '404.md',
+ },
+ routes: {
+ '404.md': expectText,
+ },
+ });
+
+ await page.evaluate(() => (window.location.hash = '#/fail'));
+ await expect(page.locator('#main')).toContainText(expectText);
+ });
});
});
diff --git a/test/e2e/plugins.test.js b/test/e2e/plugins.test.js
index ebf6d3ca9..68534830d 100644
--- a/test/e2e/plugins.test.js
+++ b/test/e2e/plugins.test.js
@@ -1,4 +1,5 @@
import docsifyInit from '../helpers/docsify-init.js';
+import { waitForFunction } from '../helpers/wait-for.js';
import { test, expect } from './fixtures/docsify-init-fixture.js';
test.describe('Plugins', () => {
@@ -72,84 +73,155 @@ test.describe('Plugins', () => {
expect(consoleMsgs).toEqual(expectedMsgs);
});
- test('beforeEach() return value', async ({ page }) => {
- await docsifyInit({
- config: {
- plugins: [
- function (hook, vm) {
- hook.beforeEach(markdown => {
- return 'beforeEach';
- });
- },
- ],
- },
- // _logHTML: true,
+ test.describe('beforeEach()', () => {
+ test('return value', async ({ page }) => {
+ await docsifyInit({
+ config: {
+ plugins: [
+ function (hook, vm) {
+ hook.beforeEach(markdown => {
+ return 'beforeEach';
+ });
+ },
+ ],
+ },
+ // _logHTML: true,
+ });
+
+ await expect(page.locator('#main')).toContainText('beforeEach');
});
- await expect(page.locator('#main')).toContainText('beforeEach');
+ test('async return value', async ({ page }) => {
+ await docsifyInit({
+ config: {
+ plugins: [
+ function (hook, vm) {
+ hook.beforeEach((markdown, next) => {
+ setTimeout(() => {
+ next('beforeEach');
+ }, 100);
+ });
+ },
+ ],
+ },
+ markdown: {
+ homepage: '# Hello World',
+ },
+ // _logHTML: true,
+ });
+
+ await expect(page.locator('#main')).toContainText('beforeEach');
+ });
});
- test('beforeEach() async return value', async ({ page }) => {
- await docsifyInit({
- config: {
- plugins: [
- function (hook, vm) {
- hook.beforeEach((markdown, next) => {
- setTimeout(() => {
- next('beforeEach');
- }, 100);
- });
- },
- ],
- },
- markdown: {
- homepage: '# Hello World',
- },
- // _logHTML: true,
+ test.describe('afterEach()', () => {
+ test('return value', async ({ page }) => {
+ await docsifyInit({
+ config: {
+ plugins: [
+ function (hook, vm) {
+ hook.afterEach(html => {
+ return 'afterEach
';
+ });
+ },
+ ],
+ },
+ markdown: {
+ homepage: '# Hello World',
+ },
+ // _logHTML: true,
+ });
+
+ await expect(page.locator('#main')).toContainText('afterEach');
});
- await expect(page.locator('#main')).toContainText('beforeEach');
+ test('async return value', async ({ page }) => {
+ await docsifyInit({
+ config: {
+ plugins: [
+ function (hook, vm) {
+ hook.afterEach((html, next) => {
+ setTimeout(() => {
+ next('afterEach
');
+ }, 100);
+ });
+ },
+ ],
+ },
+ markdown: {
+ homepage: '# Hello World',
+ },
+ // _logHTML: true,
+ });
+
+ await expect(page.locator('#main')).toContainText('afterEach');
+ });
});
- test('afterEach() return value', async ({ page }) => {
- await docsifyInit({
- config: {
- plugins: [
- function (hook, vm) {
- hook.afterEach(html => {
- return 'afterEach
';
- });
- },
- ],
- },
- markdown: {
- homepage: '# Hello World',
- },
- // _logHTML: true,
+ test.describe('route data accessible to plugins', () => {
+ let routeData = null;
+
+ test.beforeEach(async ({ page }) => {
+ // Store route data set via plugin hook (below)
+ page.on('console', async msg => {
+ for (const arg of msg.args()) {
+ const val = await arg.jsonValue();
+ const obj = JSON.parse(val);
+ obj.response && (routeData = obj);
+ }
+ });
});
- await expect(page.locator('#main')).toContainText('afterEach');
- });
+ test.afterEach(async ({ page }) => {
+ routeData = null;
+ });
- test('afterEach() async return value', async ({ page }) => {
- await docsifyInit({
- config: {
- plugins: [
- function (hook, vm) {
- hook.afterEach((html, next) => {
- setTimeout(() => {
- next('afterEach
');
- }, 100);
- });
- },
- ],
- },
- markdown: {
- homepage: '# Hello World',
- },
- // _logHTML: true,
+ test('success (200)', async ({ page }) => {
+ await docsifyInit({
+ config: {
+ plugins: [
+ function (hook, vm) {
+ hook.doneEach(html => {
+ console.log(JSON.stringify(vm.route));
+ });
+ },
+ ],
+ },
+ });
+
+ expect(routeData).toHaveProperty('response');
+ expect(routeData.response).toHaveProperty('ok', true);
+ expect(routeData.response).toHaveProperty('status', 200);
+ expect(routeData.response).toHaveProperty('statusText', 'OK');
});
- await expect(page.locator('#main')).toContainText('afterEach');
+ test('fail (404)', async ({ page }) => {
+ const link404Elm = page.locator('a[href*="404"]');
+
+ await docsifyInit({
+ markdown: {
+ sidebar: `
+ - [404](404.md)
+ `,
+ },
+ config: {
+ plugins: [
+ function (hook, vm) {
+ hook.doneEach(html => {
+ console.log(JSON.stringify(vm.route));
+ });
+ },
+ ],
+ },
+ });
+
+ await link404Elm.click();
+ await waitForFunction(() => routeData?.response?.status === 404);
+
+ expect(routeData).toHaveProperty('response');
+ expect(routeData.response).toHaveProperty('ok', false);
+ expect(routeData.response).toHaveProperty('status', 404);
+ expect(routeData.response).toHaveProperty('statusText', 'Not Found');
+ });
});
});
diff --git a/test/e2e/virtual-routes.test.js b/test/e2e/virtual-routes.test.js
index b84ca30ca..0b2e3f86c 100644
--- a/test/e2e/virtual-routes.test.js
+++ b/test/e2e/virtual-routes.test.js
@@ -221,7 +221,7 @@ test.describe('Virtual Routes - Generate Dynamic Content via Config', () => {
await navigateToRoute(page, '/d');
const mainElm = page.locator('#main');
- await expect(mainElm).toContainText('404 - Not found');
+ await expect(mainElm).toContainText('404 - Not Found');
});
test('skip routes that returned a falsy value that is not a boolean', async ({
@@ -263,7 +263,7 @@ test.describe('Virtual Routes - Generate Dynamic Content via Config', () => {
await navigateToRoute(page, '/multiple/matches');
const mainElm = page.locator('#main');
- await expect(mainElm).toContainText('404 - Not found');
+ await expect(mainElm).toContainText('404 - Not Found');
});
test('skip routes that are not a valid string or function', async ({