From 0f3b245c8e147d06b2079aadc3bf5961d45b4def Mon Sep 17 00:00:00 2001 From: falsandtru Date: Mon, 4 Sep 2023 20:49:41 +0900 Subject: [PATCH] Add FakeXMLHttpRequest --- CHANGELOG.md | 6 ++ README.md | 5 +- gh-pages/_includes/nav.html | 7 ++- gh-pages/docs/apis/index.md | 4 ++ gh-pages/docs/apis/pjax/config/index.md | 4 +- gh-pages/docs/apis/util/index.md | 31 ++++++++++ gh-pages/index.md | 2 + index.d.ts | 8 ++- src/export.ts | 1 + src/layer/domain/data/config.ts | 2 +- src/layer/domain/router/module/fetch/xhr.ts | 7 ++- src/layer/domain/router/module/update.ts | 2 +- src/lib/xhr.ts | 58 +++++++++++++++++++ test/integration/config/fetch.rewrite.test.ts | 34 ++++------- .../integration/config/update.rewrite.test.ts | 13 +++-- test/interface/index.test.ts | 8 ++- 16 files changed, 148 insertions(+), 44 deletions(-) create mode 100644 gh-pages/docs/apis/util/index.md create mode 100644 src/lib/xhr.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index e830754d..cf4b6d94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 3.40.0 + +- Add FakeXMLHttpRequest. +- Change `fetch.rewrite` option. +- Change `update.rewrite` option. + ## 3.39.1 - Fix noscript parsing. diff --git a/README.md b/README.md index 97816765..926f24a4 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,8 @@ Most SPA frameworks and pjax libraries lack many essential functions to keep the |Execution sequence keeping| | |✓| | |Non-blocking script load|✓|✓|✓| | |**Subresource integrity verification**| | |✓\*1| | -|Lightweight source rewrite| |✓|✓| | +|**Rewrite request and response (XHR)**| | |✓| | +|Rewrite source document| |✓|✓| | |ETag support| | |✓| | |Cache|✓|✓|✓|✓| |URL scope| | |✓|✓| @@ -43,7 +44,7 @@ Most SPA frameworks and pjax libraries lack many essential functions to keep the |**Browser history fix**| | |✓| | |**Scroll position restoration**| | |✓| | |**Unexpected scroll prevention**| | |✓| | -|NOSCRIPT tag restoration| | |✓| | +|NOSCRIPT tag fix| | |✓| | |History API support\*2| | |✓| | |No jQuery dependency| | |✓|✓| diff --git a/gh-pages/_includes/nav.html b/gh-pages/_includes/nav.html index c4731478..76791540 100644 --- a/gh-pages/_includes/nav.html +++ b/gh-pages/_includes/nav.html @@ -6,8 +6,13 @@
  • APIs
  • diff --git a/gh-pages/docs/apis/index.md b/gh-pages/docs/apis/index.md index f5013468..09893bfd 100644 --- a/gh-pages/docs/apis/index.md +++ b/gh-pages/docs/apis/index.md @@ -15,3 +15,7 @@ Pjax APIs. ## [Events]({{ site.basepath }}docs/apis/events/) Global events. + +## [Util]({{ site.basepath }}docs/apis/util/) + +Utilities. diff --git a/gh-pages/docs/apis/pjax/config/index.md b/gh-pages/docs/apis/pjax/config/index.md index fb6f2fb5..bccdd63d 100644 --- a/gh-pages/docs/apis/pjax/config/index.md +++ b/gh-pages/docs/apis/pjax/config/index.md @@ -65,7 +65,7 @@ Set a dictionary object having has/get/set/delete methods of Map to pass the doc ## fetch: {...} = ... -### rewrite: (path: string, method: string, headers: Headers, timeout: number, body: FormData | null) => XMLHttpRequest +### rewrite: (url: string, method: string, headers: Headers, timeout: number, body: FormData | null) => XMLHttpRequest | undefined Rewrite the XHR object, or replace it with another or fake. @@ -79,7 +79,7 @@ Wait for the specified milliseconds after sending a request. ## update: {...} = ... -### rewrite: (doc: Document, area: string, memory?: Document) => void = `() => undefined` +### rewrite: (url: string, document: Document, area: string, cache?: Document) => void = `() => undefined` Rewrite the source document object. If you use the sequence option, you should use only it instead of this. diff --git a/gh-pages/docs/apis/util/index.md b/gh-pages/docs/apis/util/index.md new file mode 100644 index 00000000..7dd333a0 --- /dev/null +++ b/gh-pages/docs/apis/util/index.md @@ -0,0 +1,31 @@ +--- +layout: layout +title: Util +type: page +nav: nav +class: style-api style-api-detail +--- + +# Util + +## FakeXMLHttpRequest + +Make a fake XHR object that doesn't send a request. + +```ts +import Pjax, { FakeXMLHttpRequest } from 'pjax-api'; + +new Pjax({ + fetch: { + rewrite: url => + FakeXMLHttpRequest.create( + url, + fetch(url, { headers: { 'Content-Type': 'application/json' } }) + .then(res => res.json()) + .then(data => + new DOMParser().parseFromString( + `${data.title}${data.body}`, + 'text/html'))), + }, +}); +``` diff --git a/gh-pages/index.md b/gh-pages/index.md index fc937b42..d59f0f03 100644 --- a/gh-pages/index.md +++ b/gh-pages/index.md @@ -29,7 +29,9 @@ This site is also powered by PJAX as a demo. Try page transitions. - [APIs]({{ site.basepath }}docs/apis/) - [Pjax]({{ site.basepath }}docs/apis/pjax/) + - [Config]({{ site.basepath }}docs/apis/pjax/config/) - [Events]({{ site.basepath }}docs/apis/events/) + - [Util]({{ site.basepath }}docs/apis/util/)
    diff --git a/index.d.ts b/index.d.ts index 4fc39b64..1fd6855b 100644 --- a/index.d.ts +++ b/index.d.ts @@ -23,13 +23,13 @@ export interface Config { readonly cache?: Dict; readonly memory?: Dict; readonly fetch?: { - readonly rewrite?: (path: string, method: string, headers: Headers, timeout: number, body: FormData | null) => XMLHttpRequest; + readonly rewrite?: (url: string, method: string, headers: Headers, timeout: number, body: FormData | null) => XMLHttpRequest | undefined; readonly headers?: Headers; readonly timeout?: number; readonly wait?: number; }; readonly update?: { - readonly rewrite?: (path: string, doc: Document, area: string, memory: Document | undefined) => void; + readonly rewrite?: (url: string, document: Document, area: string, cache: Document | undefined) => void; readonly head?: string; readonly css?: boolean; readonly script?: boolean; @@ -63,3 +63,7 @@ declare global { } } + +export class FakeXMLHttpRequest extends XMLHttpRequest { + public static create(url: string, response: Document | PromiseLike): FakeXMLHttpRequest; +} diff --git a/src/export.ts b/src/export.ts index 512130e0..1c793b6a 100644 --- a/src/export.ts +++ b/src/export.ts @@ -1 +1,2 @@ export { GUI as Pjax, GUI as default } from './layer/interface/service/gui'; +export { FakeXMLHttpRequest } from './lib/xhr'; diff --git a/src/layer/domain/data/config.ts b/src/layer/domain/data/config.ts index bcddf1c2..7cd23e2c 100644 --- a/src/layer/domain/data/config.ts +++ b/src/layer/domain/data/config.ts @@ -54,7 +54,7 @@ export class Config implements Option { wait: 0, }; public readonly update = { - rewrite: (_path: string, _doc: Document, _area: string, _memory: Document | undefined): void => undefined, + rewrite: (_url: string, _document: Document, _area: string, _cache: Document | undefined): void => undefined, head: 'base, meta, link', css: true, script: true, diff --git a/src/layer/domain/router/module/fetch/xhr.ts b/src/layer/domain/router/module/fetch/xhr.ts index 391e5919..a52f91b0 100644 --- a/src/layer/domain/router/module/fetch/xhr.ts +++ b/src/layer/domain/router/module/fetch/xhr.ts @@ -25,7 +25,8 @@ export function xhr( headers.set('If-None-Match', cache.get(displayURL.path)!.etag); } return new AtomicPromise>(resolve => { - const xhr = rewrite(displayURL.path, method, headers, timeout, body); + const xhr = rewrite(displayURL.href, method, headers, timeout, body) ?? + request(displayURL.href, method, headers, timeout, body); if (xhr.responseType !== 'document') throw new Error(`Response type must be 'document'`); @@ -77,14 +78,14 @@ export function xhr( } function request( - path: URL.Path, + url: URL.Href, method: RouterEventMethod, headers: Headers, timeout: number, body: FormData | null, ): XMLHttpRequest { const xhr = new XMLHttpRequest(); - xhr.open(method, path, true); + xhr.open(method, url, true); for (const [name, value] of headers) { xhr.setRequestHeader(name, value); } diff --git a/src/layer/domain/router/module/update.ts b/src/layer/domain/router/module/update.ts index bd68f11d..22967fd4 100644 --- a/src/layer/domain/router/module/update.ts +++ b/src/layer/domain/router/module/update.ts @@ -54,7 +54,7 @@ export function update( ? config.memory?.get(event.location.dest.path) : undefined; config.update.rewrite( - event.location.dest.path, + event.location.dest.href, documents.src, area, memory && separate({ src: memory, dst: documents.dst }, [area]).extract(() => false) diff --git a/src/lib/xhr.ts b/src/lib/xhr.ts new file mode 100644 index 00000000..4824da2c --- /dev/null +++ b/src/lib/xhr.ts @@ -0,0 +1,58 @@ +import { AtomicPromise } from 'spica/promise'; + +export class FakeXMLHttpRequest extends XMLHttpRequest { + public static create(url: string, response: Document | PromiseLike): FakeXMLHttpRequest { + const xhr = new FakeXMLHttpRequest(); + AtomicPromise.resolve(response) + .then(response => { + Object.defineProperties(xhr, { + responseURL: { + value: url, + }, + responseXML: { + value: response, + }, + }); + xhr.send(); + }); + return xhr; + } + constructor() { + super(); + this.responseType = 'document'; + } + public override send(_?: Document | XMLHttpRequestBodyInit | null | undefined): void { + let state = 3; + Object.defineProperties(this, { + readyState: { + get: () => state, + }, + status: { + value: 200, + }, + statusText: { + value: 'OK', + }, + response: { + get: () => + this.responseType === 'document' + ? this.responseXML + : this.responseText, + }, + }) + setTimeout(() => { + this.dispatchEvent(new ProgressEvent('loadstart')); + state = 4; + this.dispatchEvent(new ProgressEvent('loadend')); + this.dispatchEvent(new ProgressEvent('load')); + }); + } + public override getResponseHeader(name: string): string | null { + switch (name.toLowerCase()) { + case 'content-type': + return 'text/html'; + default: + return null; + } + } +} diff --git a/test/integration/config/fetch.rewrite.test.ts b/test/integration/config/fetch.rewrite.test.ts index 3900682c..ebdc7cc5 100644 --- a/test/integration/config/fetch.rewrite.test.ts +++ b/test/integration/config/fetch.rewrite.test.ts @@ -1,6 +1,7 @@ -import { Pjax } from '../../../index'; +import { Pjax, FakeXMLHttpRequest } from '../../../index'; import { route as router } from '../../../src/layer/interface/service/router'; import { parse } from '../../../src/lib/html'; +import { wait } from 'spica/timer'; import { once } from 'typed-dom'; describe('Integration: Config', function () { @@ -10,33 +11,18 @@ describe('Integration: Config', function () { describe('fetch.rewrite', function () { it('', function (done) { - const FakeXMLHttpRequest = XMLHttpRequest; const url = '/base/test/integration/fixture/basic/1.html'; const document = parse('').extract(); new Pjax({ fetch: { - rewrite: (path, method, headers, timeout, body) => { - const xhr = new FakeXMLHttpRequest(); - xhr.open(method, path, true); - for (const [name, value] of headers) { - xhr.setRequestHeader(name, value); - } - - xhr.responseType = 'document'; - xhr.timeout = timeout; - xhr.send(body); - - Object.defineProperties(xhr, { - responseURL: { - value: url, - }, - responseXML: { - value: parse('Title 2
    Primary 2
    ').extract(), - }, - }); - return xhr; - }, - } + rewrite: url => + FakeXMLHttpRequest.create( + url, + wait(100).then(() => + new DOMParser().parseFromString( + 'Title 2
    Primary 2
    ', + 'text/html'))), + }, }, { document, router }) .assign(url); once(document, 'pjax:ready', () => { diff --git a/test/integration/config/update.rewrite.test.ts b/test/integration/config/update.rewrite.test.ts index bb576ad7..c44f2e34 100644 --- a/test/integration/config/update.rewrite.test.ts +++ b/test/integration/config/update.rewrite.test.ts @@ -16,13 +16,13 @@ describe('Integration: Config', function () { new Pjax({ memory: new Cache(100), update: { - rewrite(path, doc, area, memory) { - switch (path) { - case url: - memory && doc.querySelector(area)?.replaceWith(memory.querySelector(area)!.cloneNode(true)); + rewrite(url, doc, area, cache) { + switch (url.split('/').at(-1)) { + case '2.html': + cache && doc.querySelector(area)?.replaceWith(cache.querySelector(area)!.cloneNode(true)); } }, - } + }, }, { document, router }) .assign(url); once(document, 'pjax:ready', () => { @@ -32,7 +32,8 @@ describe('Integration: Config', function () { document.querySelector('#primary')!.textContent = 'PRIMARY 2'; once(document, 'pjax:ready', () => { assert(window.location.pathname !== url); - assert(document.title !== 'Title 2'); + assert(document.title === 'Title 1'); + assert(document.querySelector('#primary')!.textContent === 'Primary 1'); once(document, 'pjax:ready', () => { assert(window.location.pathname === url); assert(document.title === 'Title 2'); diff --git a/test/interface/index.test.ts b/test/interface/index.test.ts index 9b944d7b..f3feaec5 100644 --- a/test/interface/index.test.ts +++ b/test/interface/index.test.ts @@ -1,9 +1,9 @@ -import _Pjax, { Pjax } from '../../index'; +import Pjax$, { Pjax, FakeXMLHttpRequest } from '../../index'; describe('Interface: Package', function () { describe('default', function () { it('default', function () { - assert(_Pjax === Pjax); + assert(Pjax$ === Pjax); }); }); @@ -19,4 +19,8 @@ describe('Interface: Package', function () { }); + describe('FakeXMLHttpRequest', function () { + assert(typeof FakeXMLHttpRequest === 'function'); + }); + });