From 73f29d05c4dea44c924dd1f01cbdd2009c69e576 Mon Sep 17 00:00:00 2001 From: John Hildenbiddle Date: Mon, 30 Oct 2023 17:34:26 -0500 Subject: [PATCH 1/7] Add response data to route object --- src/core/fetch/index.js | 6 ++++-- src/core/router/history/hash.js | 1 + src/core/router/history/html5.js | 1 + src/core/util/ajax.js | 24 +++++++++++++++++------- 4 files changed, 23 insertions(+), 9 deletions(-) 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/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)); } }); }, From 863ee474cda740e0487b52502aaea9952a340cfe Mon Sep 17 00:00:00 2001 From: John Hildenbiddle Date: Mon, 30 Oct 2023 22:50:46 -0500 Subject: [PATCH 2/7] Display response errors and process with plugins --- docs/configuration.md | 2 +- src/core/render/index.js | 12 ++++++------ test/e2e/virtual-routes.test.js | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) 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/render/index.js b/src/core/render/index.js index 2ee9755be..c51073af4 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 envrionments + // where XMLHttpRequest has been modified or mocked + if (response && !response.ok) { + text = `# ${response.status} - ${response.statusText}`; } this.callHook('beforeEach', text, result => { 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 ({ From d28f59e635f958ba4aa3062cb133f1679060f4b1 Mon Sep 17 00:00:00 2001 From: John Hildenbiddle Date: Tue, 14 Nov 2023 14:28:01 -0600 Subject: [PATCH 3/7] Add route data tests --- test/e2e/virtual-routes.test.js | 46 +++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/test/e2e/virtual-routes.test.js b/test/e2e/virtual-routes.test.js index 0b2e3f86c..08f72467e 100644 --- a/test/e2e/virtual-routes.test.js +++ b/test/e2e/virtual-routes.test.js @@ -1,5 +1,6 @@ import docsifyInit from '../helpers/docsify-init.js'; import { test, expect } from './fixtures/docsify-init-fixture.js'; +import { waitForFunction } from '../helpers/wait-for.js'; /** * Navigate to a specific route in the site @@ -290,4 +291,49 @@ test.describe('Virtual Routes - Generate Dynamic Content via Config', () => { await expect(titleElm).toContainText('Last Match'); }); }); + + test.describe('Route Data', () => { + test('route data accessible to plugins', async ({ page }) => { + const failLink = page.locator('a[href*="fail"]'); + + let routeData = null; + + // 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 docsifyInit({ + markdown: { + homepage: '[Fail](fail.md)', + }, + 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 failLink.click(); + await waitForFunction(() => routeData?.response?.status !== 200); + + expect(routeData).toHaveProperty('response'); + expect(routeData.response).toHaveProperty('ok', false); + expect(routeData.response).toHaveProperty('status', 404); + expect(routeData.response).toHaveProperty('statusText', 'Not Found'); + }); + }); }); From 3e4071437719ec6b59b2a923d9f20cd3e2f54bc7 Mon Sep 17 00:00:00 2001 From: John Hildenbiddle Date: Thu, 16 Nov 2023 14:41:55 -0600 Subject: [PATCH 4/7] Fix incorrect default value for noFoundPage --- src/core/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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: '', From a2539a627570c767eb66fd4e1f8a15ce96d3a26f Mon Sep 17 00:00:00 2001 From: John Hildenbiddle Date: Thu, 16 Nov 2023 15:17:44 -0600 Subject: [PATCH 5/7] Relocate route data tests --- test/e2e/plugins.test.js | 204 +++++++++++++++++++++----------- test/e2e/virtual-routes.test.js | 46 ------- 2 files changed, 138 insertions(+), 112 deletions(-) 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 08f72467e..0b2e3f86c 100644 --- a/test/e2e/virtual-routes.test.js +++ b/test/e2e/virtual-routes.test.js @@ -1,6 +1,5 @@ import docsifyInit from '../helpers/docsify-init.js'; import { test, expect } from './fixtures/docsify-init-fixture.js'; -import { waitForFunction } from '../helpers/wait-for.js'; /** * Navigate to a specific route in the site @@ -291,49 +290,4 @@ test.describe('Virtual Routes - Generate Dynamic Content via Config', () => { await expect(titleElm).toContainText('Last Match'); }); }); - - test.describe('Route Data', () => { - test('route data accessible to plugins', async ({ page }) => { - const failLink = page.locator('a[href*="fail"]'); - - let routeData = null; - - // 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 docsifyInit({ - markdown: { - homepage: '[Fail](fail.md)', - }, - 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 failLink.click(); - await waitForFunction(() => routeData?.response?.status !== 200); - - expect(routeData).toHaveProperty('response'); - expect(routeData.response).toHaveProperty('ok', false); - expect(routeData.response).toHaveProperty('status', 404); - expect(routeData.response).toHaveProperty('statusText', 'Not Found'); - }); - }); }); From cad4a95cef27924eb625b12e0e279e06d2e19c2c Mon Sep 17 00:00:00 2001 From: John Hildenbiddle Date: Thu, 16 Nov 2023 16:20:57 -0600 Subject: [PATCH 6/7] Handle notFoundPage 404 content --- src/core/render/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/render/index.js b/src/core/render/index.js index c51073af4..2566a1920 100644 --- a/src/core/render/index.js +++ b/src/core/render/index.js @@ -308,9 +308,9 @@ export function Render(Base) { _renderMain(text, opt = {}, next) { const { response } = this.route; - // Note: It is possible for the response to be undefined in envrionments + // Note: It is possible for the response to be undefined in environments // where XMLHttpRequest has been modified or mocked - if (response && !response.ok) { + if (response && !response.ok && (!text || response.status !== 404)) { text = `# ${response.status} - ${response.statusText}`; } From 135bbee1f46bed44e20cedf3279b58beefa532d6 Mon Sep 17 00:00:00 2001 From: John Hildenbiddle Date: Thu, 16 Nov 2023 16:21:32 -0600 Subject: [PATCH 7/7] Add notFoundPage tests --- test/e2e/configuration.test.js | 180 ++++++++++++++++++++++++--------- 1 file changed, 131 insertions(+), 49 deletions(-) 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); + }); }); });