From 767f639dc7430b814f1aad8fca545b0cc41eafc1 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Fri, 18 Dec 2020 12:15:25 -0800 Subject: [PATCH 01/28] Add schedule update function in preparation for UI --- scripts/bundles/controls/mockFetch.js | 121 ++++++++++++++++++-------- 1 file changed, 83 insertions(+), 38 deletions(-) diff --git a/scripts/bundles/controls/mockFetch.js b/scripts/bundles/controls/mockFetch.js index ad2ffd7..55f9b8c 100644 --- a/scripts/bundles/controls/mockFetch.js +++ b/scripts/bundles/controls/mockFetch.js @@ -26,6 +26,7 @@ * @typedef Config * @property {number} durationMs * @property {boolean} areNewRequestsPaused + * @property {'auto' | 'interactive'} mode * @property {() => string} newId * @property {Timer | null} timer * @property {Map} requests @@ -38,27 +39,53 @@ export function createMockFetchConfig() { let id = 0; + /** @type {'auto' | 'interactive'} */ + let currentMode = "auto"; + return { durationMs: 3 * 1000, areNewRequestsPaused: false, + get mode() { + return currentMode; + }, + set mode(newMode) { + if (newMode !== currentMode) { + if (newMode !== "auto" && newMode !== "interactive") { + throw new Error(`Unsupported mockFetch mode: ${newMode}.`); + } + + currentMode = newMode; + } + }, + newId: () => `${++id}`, // TODO: Build request editing module // - // Normal mode: + // Normal mode (done): // - // Only keep one timeout for when the next request will need to resolved. When - // it expires, loop through all requests and resolve all that have completed. - // When a new request arrives, check if it will expire before the current - // timeout. If so, replace the current timeout with a new one for the new - // request. + // Only keep one timeout for when the next request will need to resolved. + // When it expires, loop through all requests and resolve all that have + // completed. When a new request arrives, check if it will expire before the + // current timeout. If so, replace the current timeout with a new one for + // the new request. // // Interactive mode: // // If the UI control is displayed, instead of relying to timers to resolve - // requests, we will run a animation loop to animate the UI control and expire - // requests in progress. + // requests, we will run a animation loop to animate the UI control and + // expire requests in progress. + // + // To handle the two modes, perhaps the config should have a + // `scheduleUpdate` function that either sets the timers or schedules the + // next animation loop. + // + // Consider placing the UI in a web-component that individual apps can + // render inside the app? Or perhaps the app pages can pass a flag to + // include it in the template. Perhaps createMockFetch and the web-component + // should take the config as an optional parameter that defaults to a global + // default config. timer: null, @@ -99,7 +126,9 @@ export function createMockFetchConfig() { resolveRequests(this); }, - log(...msg) {} + log(...msg) { + // By default, log nothing? + } }; } @@ -146,15 +175,8 @@ export function createMockFetch(config) { config.requests.set(request.id, request); if (!config.areNewRequestsPaused) { - const now = Date.now(); - const expiresAt = now + request.duration; - request.expiresAt = expiresAt; - - // If there is no timer or this request finishes faster than the current - // timer, setup a new timer - if (config.timer == null || request.expiresAt < config.timer.expiresAt) { - setTimer(config, request, now); - } + request.expiresAt = Date.now() + request.duration; + scheduleUpdate(config); } return promise; @@ -165,20 +187,54 @@ export function createMockFetch(config) { /** * @param {Config} config - * @param {Request} request - * @param {number} now */ -function setTimer(config, request, now) { +function scheduleUpdate(config) { + if (config.requests.size == 0) { + return; + } + + if (config.mode == "interactive") { + throw new Error("Interactive mode is not implemented"); + } else { + setTimer(config); + } +} + +/** + * @param {Config} config + */ +function setTimer(config) { + /** @type {Request} Request with the next expiration */ + let nextRequest = null; + for (let request of config.requests.values()) { + if (nextRequest == null || request.expiresAt < nextRequest.expiresAt) { + nextRequest = request; + } + } + + if (nextRequest == null) { + // If there is no request to schedule, then bail out early + return; + } + if (config.timer) { + if (config.timer.expiresAt <= nextRequest.expiresAt) { + // If there is an existing timer and it expires before or at the same time + // as the next request to expire, than there is no need to update the + // timer + return; + } + window.clearTimeout(config.timer.timeoutId); config.timer = null; } - const timeoutId = window.setTimeout( - () => resolveRequests(config), - request.expiresAt - now - ); - config.timer = { timeoutId, expiresAt: request.expiresAt }; + const timeout = nextRequest.expiresAt - Date.now(); + const timeoutId = window.setTimeout(() => { + config.timer = null; + resolveRequests(config); + }, timeout); + config.timer = { timeoutId, expiresAt: nextRequest.expiresAt }; } /** @@ -188,8 +244,6 @@ function resolveRequests(config) { const now = Date.now(); const toRemove = []; - /** @type {Request} Request with the next expiration */ - let nextRequest = null; for (let [id, request] of config.requests.entries()) { if (request.expiresAt == null) { continue; @@ -198,11 +252,6 @@ function resolveRequests(config) { // then go ahead and resolve it request.resolve(); toRemove.push(id); - } else if ( - nextRequest == null || - request.expiresAt < nextRequest.expiresAt - ) { - nextRequest = request; } } @@ -210,9 +259,5 @@ function resolveRequests(config) { config.requests.delete(id); } - if (nextRequest) { - setTimer(config, nextRequest, now); - } else { - config.timer = null; - } + scheduleUpdate(config); } From 27deb8e3145fd79dce4d16f6759efd7e3a90941b Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Fri, 18 Dec 2020 13:37:59 -0800 Subject: [PATCH 02/28] Move mockFetch types to TS def file --- scripts/bundles/controls/mockFetch.d.ts | 40 +++++++++++++++++++ scripts/bundles/controls/mockFetch.js | 51 ++++--------------------- 2 files changed, 47 insertions(+), 44 deletions(-) create mode 100644 scripts/bundles/controls/mockFetch.d.ts diff --git a/scripts/bundles/controls/mockFetch.d.ts b/scripts/bundles/controls/mockFetch.d.ts new file mode 100644 index 0000000..baca833 --- /dev/null +++ b/scripts/bundles/controls/mockFetch.d.ts @@ -0,0 +1,40 @@ +interface Timer { + expiresAt: number; + timeoutId: number; +} + +type RequestId = string; + +interface Request { + id: RequestId; + /** When this request should resolve. If null, request is paused and not scheduled to complete */ + expiresAt: number | null; + /** Total time this request should wait */ + duration: number; + /** Tracks how much time of duration has elapsed when a request is paused/resumed */ + elapsedTime: number | null; + /** Display name of request */ + name: string; + url: string; + options: RequestInit; + promise: Promise; + resolve: () => void; + reject: () => void; +} + +interface Config { + durationMs: number; + areNewRequestsPaused: boolean; + mode: "auto" | "interactive"; + timer: Timer | null; + requests: Map; + newId(): string; + pause(id: RequestId): void; + resume(id: RequestId): void; + log(...msg: any[]): void; +} + +export function createMockFetchConfig(): Config; +export function createMockFetch( + config: Config +): (url: string, options?: RequestInit) => Promise; diff --git a/scripts/bundles/controls/mockFetch.js b/scripts/bundles/controls/mockFetch.js index 55f9b8c..7c8fdc7 100644 --- a/scripts/bundles/controls/mockFetch.js +++ b/scripts/bundles/controls/mockFetch.js @@ -1,41 +1,4 @@ -/** - * @typedef Timer - * @property {number} expiresAt - * @property {number} timeoutId - */ - -/** - * @typedef {string} RequestId - * - * @typedef Request - * @property {RequestId} id - * @property {number | null} expiresAt When this request should resolve. If - * null, request is paused and not scheduled to complete - * @property {number} duration Total time this request should wait - * @property {number | null} elapsedTime Tracks how much time of duration has - * elapsed when a request is paused/resumed - * @property {string} name Display name of request - * @property {string} url - * @property {RequestInit} options - * @property {Promise} promise - * @property {() => void} resolve - * @property {() => void} reject - */ - -/** - * @typedef Config - * @property {number} durationMs - * @property {boolean} areNewRequestsPaused - * @property {'auto' | 'interactive'} mode - * @property {() => string} newId - * @property {Timer | null} timer - * @property {Map} requests - * @property {(id: RequestId) => void} pause - * @property {(id: RequestId) => void} resume - * @property {(...msg: any[]) => void} log - */ - -/** @returns {Config} */ +/** @returns {import('./mockFetch').Config} */ export function createMockFetchConfig() { let id = 0; @@ -133,7 +96,7 @@ export function createMockFetchConfig() { } /** - * @param {Config} config + * @param {import('./mockFetch').Config} config */ export function createMockFetch(config) { /** @@ -158,7 +121,7 @@ export function createMockFetch(config) { // }; }); - /** @type {Request} */ + /** @type {import('./mockFetch').Request} */ const request = { id: config.newId(), duration: config.durationMs, @@ -186,7 +149,7 @@ export function createMockFetch(config) { } /** - * @param {Config} config + * @param {import('./mockFetch').Config} config */ function scheduleUpdate(config) { if (config.requests.size == 0) { @@ -201,10 +164,10 @@ function scheduleUpdate(config) { } /** - * @param {Config} config + * @param {import('./mockFetch').Config} config */ function setTimer(config) { - /** @type {Request} Request with the next expiration */ + /** @type {import('./mockFetch').Request} Request with the next expiration */ let nextRequest = null; for (let request of config.requests.values()) { if (nextRequest == null || request.expiresAt < nextRequest.expiresAt) { @@ -238,7 +201,7 @@ function setTimer(config) { } /** - * @param {Config} config + * @param {import('./mockFetch').Config} config */ function resolveRequests(config) { const now = Date.now(); From b9c86bbf8916fd8b1e4c10db07fbcf854f69039d Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Fri, 18 Dec 2020 21:34:20 -0800 Subject: [PATCH 03/28] Add update event firing --- package-lock.json | 12 +++++ scripts/bundles/controls/mockFetch.d.ts | 5 ++ scripts/bundles/controls/mockFetch.js | 55 +++++++++++++------ scripts/package.json | 1 + tests/lib/mockFetch.test.js | 72 +++++++++++++++++++++++++ 5 files changed, 130 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index ad2a6bb..84d9b39 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11069,6 +11069,7 @@ "license": "MIT", "dependencies": { "classcat": "^4.1.0", + "mitt": "^2.1.0", "preact": "^10.4.4", "preact-router": "^3.2.1", "prismjs": "^1.23.0", @@ -11451,6 +11452,11 @@ "node": ">=10" } }, + "scripts/node_modules/mitt": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-2.1.0.tgz", + "integrity": "sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg==" + }, "scripts/node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -18981,6 +18987,7 @@ "classcat": "^4.1.0", "cssnano": "^6.0.0", "gzip-size": "^7.0.0", + "mitt": "^2.1.0", "nodemon": "^3.0.0", "postcss": "^8.0.0", "postcss-reporter": "^7.0.5", @@ -19233,6 +19240,11 @@ "brace-expansion": "^2.0.1" } }, + "mitt": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-2.1.0.tgz", + "integrity": "sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg==" + }, "nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", diff --git a/scripts/bundles/controls/mockFetch.d.ts b/scripts/bundles/controls/mockFetch.d.ts index baca833..d9c9eed 100644 --- a/scripts/bundles/controls/mockFetch.d.ts +++ b/scripts/bundles/controls/mockFetch.d.ts @@ -22,6 +22,8 @@ interface Request { reject: () => void; } +type MockFetchEventType = "update"; + interface Config { durationMs: number; areNewRequestsPaused: boolean; @@ -31,7 +33,10 @@ interface Config { newId(): string; pause(id: RequestId): void; resume(id: RequestId): void; + on(type: MockFetchEventType, handler: () => void): void; + off(type: MockFetchEventType, handler: () => void): void; log(...msg: any[]): void; + _emit(type: MockFetchEventType): void; } export function createMockFetchConfig(): Config; diff --git a/scripts/bundles/controls/mockFetch.js b/scripts/bundles/controls/mockFetch.js index 7c8fdc7..795b225 100644 --- a/scripts/bundles/controls/mockFetch.js +++ b/scripts/bundles/controls/mockFetch.js @@ -1,3 +1,5 @@ +import mitt from "mitt"; + /** @returns {import('./mockFetch').Config} */ export function createMockFetchConfig() { let id = 0; @@ -5,6 +7,8 @@ export function createMockFetchConfig() { /** @type {'auto' | 'interactive'} */ let currentMode = "auto"; + const events = mitt(); + return { durationMs: 3 * 1000, areNewRequestsPaused: false, @@ -19,6 +23,7 @@ export function createMockFetchConfig() { } currentMode = newMode; + scheduleUpdate(this); } }, @@ -68,6 +73,9 @@ export function createMockFetchConfig() { request.elapsedTime = request.duration - (request.expiresAt - now); request.expiresAt = null; + + // Reset the timer if necessary + resolveRequests(this); }, resume(id) { @@ -89,7 +97,19 @@ export function createMockFetchConfig() { resolveRequests(this); }, - log(...msg) { + on(type, handler) { + events.on(type, handler); + }, + + off(type, handler) { + events.off(type, handler); + }, + + _emit(type) { + events.emit(type); + }, + + log() { // By default, log nothing? } }; @@ -142,6 +162,7 @@ export function createMockFetch(config) { scheduleUpdate(config); } + config._emit("update"); return promise; } @@ -170,28 +191,31 @@ function setTimer(config) { /** @type {import('./mockFetch').Request} Request with the next expiration */ let nextRequest = null; for (let request of config.requests.values()) { - if (nextRequest == null || request.expiresAt < nextRequest.expiresAt) { + if ( + request.expiresAt != null && + (nextRequest == null || request.expiresAt < nextRequest.expiresAt) + ) { nextRequest = request; } } - if (nextRequest == null) { - // If there is no request to schedule, then bail out early - return; - } - - if (config.timer) { - if (config.timer.expiresAt <= nextRequest.expiresAt) { - // If there is an existing timer and it expires before or at the same time - // as the next request to expire, than there is no need to update the - // timer - return; - } - + if ( + config.timer && + (nextRequest == null || nextRequest.expiresAt !== config.timer.expiresAt) + ) { + // If there is an existing timer, and no next request or the timer expires a + // different time than the next request, clear the exiting timer. window.clearTimeout(config.timer.timeoutId); config.timer = null; } + if ( + nextRequest == null || + (config.timer && nextRequest.expiresAt === config.timer.expiresAt) + ) { + return; + } + const timeout = nextRequest.expiresAt - Date.now(); const timeoutId = window.setTimeout(() => { config.timer = null; @@ -223,4 +247,5 @@ function resolveRequests(config) { } scheduleUpdate(config); + config._emit("update"); } diff --git a/scripts/package.json b/scripts/package.json index 1a593c4..03c93f6 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -9,6 +9,7 @@ }, "dependencies": { "classcat": "^4.1.0", + "mitt": "^2.1.0", "preact": "^10.4.4", "preact-router": "^3.2.1", "prismjs": "^1.23.0", diff --git a/tests/lib/mockFetch.test.js b/tests/lib/mockFetch.test.js index e1b2abc..4713a9a 100644 --- a/tests/lib/mockFetch.test.js +++ b/tests/lib/mockFetch.test.js @@ -14,12 +14,18 @@ describe("mockFetch library", () => { /** @type {ReturnType} */ let mockFetch; + /** @type {jest.Mock} */ + let updateCallback; + beforeEach(() => { jest.useFakeTimers("modern"); + updateCallback = jest.fn(); + // @ts-expect-error config = createMockFetchConfig(); config.log = jest.fn(); + config.on("update", updateCallback); mockFetch = createMockFetch(config); }); @@ -44,34 +50,41 @@ describe("mockFetch library", () => { it("should resolve two requests created closely together with same duration", async () => { const req1 = mockFetch("/test/req1").then(() => 1); + jest.advanceTimersByTime(1); const req2 = mockFetch("/test/req2").then(() => 2); + expect(updateCallback).toHaveBeenCalledTimes(2); jest.advanceTimersByTime(config.durationMs + 1); const [result1, result2] = await Promise.all([req1, req2]); expect(result1).toBe(1); expect(result2).toBe(2); + expect(updateCallback).toHaveBeenCalledTimes(3); }); it("should resolve two requests created at the same time with longer durations", async () => { // Create first request const req1Duration = config.durationMs; const req1 = mockFetch("/test/req1").then(() => 1); + expect(updateCallback).toHaveBeenCalledTimes(1); // Create second request with longer duration config.durationMs = req1Duration * 2; const req2 = mockFetch("/test/req2").then(() => 2); + expect(updateCallback).toHaveBeenCalledTimes(2); // Advance time enough such only first request should complete jest.advanceTimersByTime(req1Duration + 1); expect(config.log).toHaveBeenCalledTimes(1); expect(config.log).toHaveBeenNthCalledWith(1, "Resolving GET /test/req1"); + expect(updateCallback).toHaveBeenCalledTimes(3); expect(await req1).toBe(1); // Advance time by remaining time such that second request should complete jest.advanceTimersByTime(req1Duration + 1); expect(config.log).toHaveBeenCalledTimes(2); expect(config.log).toHaveBeenNthCalledWith(2, "Resolving GET /test/req2"); + expect(updateCallback).toHaveBeenCalledTimes(4); expect(await req2).toBe(2); }); @@ -79,31 +92,37 @@ describe("mockFetch library", () => { // Create first request const req1Duration = config.durationMs; const req1 = mockFetch("/test/req1").then(() => 1); + expect(updateCallback).toHaveBeenCalledTimes(1); // Create second request with shorter duration than first config.durationMs = req1Duration / 2; const req2 = mockFetch("/test/req2").then(() => 2); + expect(updateCallback).toHaveBeenCalledTimes(2); // Advance time such that only second request should complete jest.advanceTimersByTime(req1Duration / 2 + 1); expect(config.log).toHaveBeenCalledTimes(1); expect(config.log).toHaveBeenNthCalledWith(1, "Resolving GET /test/req2"); + expect(updateCallback).toHaveBeenCalledTimes(3); expect(await req2).toBe(2); // Advance time by remaining time such that first request should complete jest.advanceTimersByTime(req1Duration / 2 + 1); expect(config.log).toHaveBeenCalledTimes(2); expect(config.log).toHaveBeenNthCalledWith(2, "Resolving GET /test/req1"); + expect(updateCallback).toHaveBeenCalledTimes(4); expect(await req1).toBe(1); }); it("should resolve two requests created far apart", async () => { // Create initial request const req1 = mockFetch("/test/req1").then(() => 1); + expect(updateCallback).toHaveBeenCalledTimes(1); // Create another request some time later jest.advanceTimersByTime(config.durationMs / 2); const req2 = mockFetch("/test/req2").then(() => 2); + expect(updateCallback).toHaveBeenCalledTimes(2); // Advance time so that all requests should expire jest.advanceTimersByTime(config.durationMs + 1); @@ -111,12 +130,14 @@ describe("mockFetch library", () => { const [result1, result2] = await Promise.all([req1, req2]); expect(result1).toBe(1); expect(result2).toBe(2); + expect(updateCallback).toHaveBeenCalledTimes(4); }); it("should resolve three requests created far apart with varying durations", async () => { // Create first request const req1Duration = config.durationMs; const req1 = mockFetch("/test/req1").then(() => 1); + expect(updateCallback).toHaveBeenCalledTimes(1); // Create a second and third request some time later jest.advanceTimersByTime(req1Duration / 2); @@ -125,27 +146,32 @@ describe("mockFetch library", () => { const req2Duration = req1Duration * 2; config.durationMs = req2Duration; const req2 = mockFetch("/test/req2").then(() => 2); + expect(updateCallback).toHaveBeenCalledTimes(2); // Create third request with half duration than second config.durationMs = req2Duration / 2; const req3 = mockFetch("/test/req3").then(() => 3); + expect(updateCallback).toHaveBeenCalledTimes(3); // Advance time such that only first request should complete jest.advanceTimersByTime(req1Duration / 2 + 1); expect(config.log).toHaveBeenCalledTimes(1); expect(config.log).toHaveBeenNthCalledWith(1, "Resolving GET /test/req1"); + expect(updateCallback).toHaveBeenCalledTimes(4); expect(await req1).toBe(1); // Advance time such that only the third request should complete jest.advanceTimersByTime(req1Duration / 2 + 1); expect(config.log).toHaveBeenCalledTimes(2); expect(config.log).toHaveBeenNthCalledWith(2, "Resolving GET /test/req3"); + expect(updateCallback).toHaveBeenCalledTimes(5); expect(await req3).toBe(3); // Advance time by remaining time such that second request should complete jest.advanceTimersByTime(req1Duration + 1); expect(config.log).toHaveBeenCalledTimes(3); expect(config.log).toHaveBeenNthCalledWith(3, "Resolving GET /test/req2"); + expect(updateCallback).toHaveBeenCalledTimes(6); expect(await req2).toBe(2); }); @@ -153,6 +179,7 @@ describe("mockFetch library", () => { // Create first request const req1Duration = config.durationMs; const req1 = mockFetch("/test/req1").then(() => 1); + expect(updateCallback).toHaveBeenCalledTimes(1); // Create a second and third request some time later jest.advanceTimersByTime(req1Duration / 2); @@ -161,27 +188,32 @@ describe("mockFetch library", () => { const req2Duration = req1Duration; config.durationMs = req2Duration; const req2 = mockFetch("/test/req2").then(() => 2); + expect(updateCallback).toHaveBeenCalledTimes(2); // Create third request with half duration than second config.durationMs = req2Duration * 2; const req3 = mockFetch("/test/req3").then(() => 3); + expect(updateCallback).toHaveBeenCalledTimes(3); // Advance time such that only first request should complete jest.advanceTimersByTime(req1Duration / 2 + 1); expect(config.log).toHaveBeenCalledTimes(1); expect(config.log).toHaveBeenNthCalledWith(1, "Resolving GET /test/req1"); + expect(updateCallback).toHaveBeenCalledTimes(4); expect(await req1).toBe(1); // Advance time such that only the second request should complete jest.advanceTimersByTime(req1Duration / 2 + 1); expect(config.log).toHaveBeenCalledTimes(2); expect(config.log).toHaveBeenNthCalledWith(2, "Resolving GET /test/req2"); + expect(updateCallback).toHaveBeenCalledTimes(5); expect(await req2).toBe(2); // Advance time by remaining time such that third request should complete jest.advanceTimersByTime(req1Duration + 1); expect(config.log).toHaveBeenCalledTimes(3); expect(config.log).toHaveBeenNthCalledWith(3, "Resolving GET /test/req3"); + expect(updateCallback).toHaveBeenCalledTimes(6); expect(await req3).toBe(3); }); @@ -189,16 +221,23 @@ describe("mockFetch library", () => { it("should pause a request past previous duration and resume request with remaining duration", async () => { // Request: ==== ==== const req1 = mockFetch("/test/req1").then(() => 1); + expect(updateCallback).toHaveBeenCalledTimes(1); + config.pause("1"); + expect(updateCallback).toHaveBeenCalledTimes(2); jest.advanceTimersByTime(config.durationMs + 1); expect(config.log).not.toHaveBeenCalled(); + expect(updateCallback).toHaveBeenCalledTimes(2); config.resume("1"); + expect(updateCallback).toHaveBeenCalledTimes(3); + jest.advanceTimersByTime(config.durationMs + 1); expect(config.log).toHaveBeenCalledTimes(1); expect(config.log).toHaveBeenCalledWith("Resolving GET /test/req1"); + expect(updateCallback).toHaveBeenCalledTimes(4); expect(await req1).toBe(1); }); @@ -213,20 +252,29 @@ describe("mockFetch library", () => { it("calling pause twice still properly resumes", async () => { const req1 = mockFetch("/test/req1").then(() => 1); + expect(updateCallback).toHaveBeenCalledTimes(1); + config.pause("1"); + expect(updateCallback).toHaveBeenCalledTimes(2); jest.advanceTimersByTime(config.durationMs); config.pause("1"); expect(config.log).not.toHaveBeenCalled(); + expect(updateCallback).toHaveBeenCalledTimes(2); jest.advanceTimersByTime(config.durationMs); expect(config.log).not.toHaveBeenCalled(); + expect(updateCallback).toHaveBeenCalledTimes(2); config.resume("1"); + expect(updateCallback).toHaveBeenCalledTimes(3); + jest.advanceTimersByTime(config.durationMs + 1); + expect(updateCallback).toHaveBeenCalledTimes(4); expect(config.log).toHaveBeenCalledTimes(1); expect(config.log).toHaveBeenCalledWith("Resolving GET /test/req1"); + expect(updateCallback).toHaveBeenCalledTimes(4); expect(await req1).toBe(1); }); @@ -256,6 +304,8 @@ describe("mockFetch library", () => { expect(config.log).toHaveBeenCalledTimes(2); expect(config.log).toHaveBeenCalledWith("Resolving GET /test/req2"); expect(await req2).toBe(2); + + expect(updateCallback).toHaveBeenCalledTimes(6); }); it("immediate pause should not block new requests", async () => { @@ -278,6 +328,8 @@ describe("mockFetch library", () => { expect(config.log).toHaveBeenCalledTimes(2); expect(config.log).toHaveBeenCalledWith("Resolving GET /test/req1"); expect(await req1).toBe(1); + + expect(updateCallback).toHaveBeenCalledTimes(6); }); it("request1 pauses before request2, and resumes and completes within the request2", async () => { @@ -316,6 +368,8 @@ describe("mockFetch library", () => { expect(config.log).toHaveBeenCalledTimes(2); expect(config.log).toHaveBeenNthCalledWith(2, "Resolving GET /req2"); expect(await req2).toBe(2); + + expect(updateCallback).toHaveBeenCalledTimes(6); }); it("request1 pauses before request2, resumes within the request2, and completes after request2", async () => { @@ -352,6 +406,8 @@ describe("mockFetch library", () => { expect(config.log).toHaveBeenCalledTimes(2); expect(config.log).toHaveBeenNthCalledWith(2, "Resolving GET /req1"); expect(await req1).toBe(1); + + expect(updateCallback).toHaveBeenCalledTimes(6); }); it("delayed pause should not block new requests", async () => { @@ -376,6 +432,8 @@ describe("mockFetch library", () => { expect(config.log).toHaveBeenCalledTimes(2); expect(config.log).toHaveBeenCalledWith("Resolving GET /test/req1"); expect(await req1).toBe(1); + + expect(updateCallback).toHaveBeenCalledTimes(6); }); it("request pauses, resumes, and completes within another requests expiration", async () => { @@ -415,6 +473,8 @@ describe("mockFetch library", () => { expect(config.log).toHaveBeenCalledTimes(2); expect(config.log).toHaveBeenNthCalledWith(2, "Resolving GET /req1"); expect(await req1).toBe(1); + + expect(updateCallback).toHaveBeenCalledTimes(6); }); it("request pauses and resumes within another request but completes after", async () => { @@ -454,6 +514,8 @@ describe("mockFetch library", () => { expect(config.log).toHaveBeenCalledTimes(2); expect(config.log).toHaveBeenNthCalledWith(2, "Resolving GET /req2"); expect(await req2).toBe(2); + + expect(updateCallback).toHaveBeenCalledTimes(6); }); it("delayed pause should not block existing requests", async () => { @@ -479,6 +541,8 @@ describe("mockFetch library", () => { expect(config.log).toHaveBeenCalledTimes(2); expect(config.log).toHaveBeenCalledWith("Resolving GET /test/req2"); expect(await req2).toBe(2); + + expect(updateCallback).toHaveBeenCalledTimes(6); }); }); @@ -490,32 +554,38 @@ describe("mockFetch library", () => { // create two requests const req1 = mockFetch("/req1").then(() => 1); const req2 = mockFetch("/req2").then(() => 2); + expect(updateCallback).toHaveBeenCalledTimes(2); // verified paused jest.advanceTimersByTime(config.durationMs + 1); expect(config.log).not.toHaveBeenCalled(); + expect(updateCallback).toHaveBeenCalledTimes(2); // turn off config.areNewRequestsPaused = false; // create another request const req3 = mockFetch("/req3").then(() => 3); + expect(updateCallback).toHaveBeenCalledTimes(3); // verify it resolves jest.advanceTimersByTime(config.durationMs + 1); expect(config.log).toHaveBeenCalledTimes(1); expect(config.log).toHaveBeenNthCalledWith(1, "Resolving GET /req3"); + expect(updateCallback).toHaveBeenCalledTimes(4); expect(await req3).toBe(3); // resume two paused requests config.resume("1"); config.resume("2"); + expect(updateCallback).toHaveBeenCalledTimes(6); // verify they resolve jest.advanceTimersByTime(config.durationMs + 1); expect(config.log).toHaveBeenCalledTimes(3); expect(config.log).toHaveBeenNthCalledWith(2, "Resolving GET /req1"); expect(config.log).toHaveBeenNthCalledWith(3, "Resolving GET /req2"); + expect(updateCallback).toHaveBeenCalledTimes(7); expect(await req1).toBe(1); expect(await req2).toBe(2); }); @@ -540,6 +610,8 @@ describe("mockFetch library", () => { expect(config.log).toHaveBeenCalledTimes(2); expect(config.log).toHaveBeenNthCalledWith(2, "Resolving GET /req2"); expect(await req2).toBe(2); + + expect(updateCallback).toHaveBeenCalledTimes(5); }); }); }); From a7ad052eec57b2f4d743aaecce2b3d9bb12639f0 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sat, 19 Dec 2020 13:06:21 -0800 Subject: [PATCH 04/28] Add initial implementaiton of fetch debugger ui --- scripts/bundles/controls/jsconfig.json | 8 + scripts/bundles/controls/jsx.d.ts | 15 + scripts/bundles/controls/mockFetchDebugger.js | 357 ++++++++++++++++++ scripts/bundles/site.js | 8 + scripts/bundles/site.scss | 10 + tests/lib/mockFetch.test.js | 2 - 6 files changed, 398 insertions(+), 2 deletions(-) create mode 100644 scripts/bundles/controls/jsconfig.json create mode 100644 scripts/bundles/controls/jsx.d.ts create mode 100644 scripts/bundles/controls/mockFetchDebugger.js diff --git a/scripts/bundles/controls/jsconfig.json b/scripts/bundles/controls/jsconfig.json new file mode 100644 index 0000000..5280256 --- /dev/null +++ b/scripts/bundles/controls/jsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "target": "es2017", + "moduleResolution": "node", + "jsx": "react", + "jsxFactory": "h" + } +} diff --git a/scripts/bundles/controls/jsx.d.ts b/scripts/bundles/controls/jsx.d.ts new file mode 100644 index 0000000..d0267ae --- /dev/null +++ b/scripts/bundles/controls/jsx.d.ts @@ -0,0 +1,15 @@ +interface JSXAttributes { + children: HTMLElement[]; +} + +type HTMLElementsMap = { + [K in keyof HTMLElementTagNameMap]: HTMLElementTagNameMap[K] & JSXAttributes; +}; + +declare namespace JSX { + interface Element extends HTMLElement {} + interface ElementChildrenAttribute { + children: Array; + } + interface IntrinsicElements extends HTMLElementsMap {} +} diff --git a/scripts/bundles/controls/mockFetchDebugger.js b/scripts/bundles/controls/mockFetchDebugger.js new file mode 100644 index 0000000..8666a46 --- /dev/null +++ b/scripts/bundles/controls/mockFetchDebugger.js @@ -0,0 +1,357 @@ +const hasOwn = Object.prototype.hasOwnProperty; + +/** @jsx h */ + +/** + * @param {string} tag + * @param {Record} attributes + * @param {Array} children + * @returns {HTMLElement} + */ +function h(tag, attributes, ...children) { + const element = document.createElement(tag); + + for (let attr in attributes) { + if (hasOwn.call(attributes, attr)) { + if (attr in element) { + element[attr] = attributes[attr]; + } else { + element.setAttribute(attr, attributes[attr]); + } + } + } + + for (let child of children) { + if (typeof child == "string") { + element.appendChild(document.createTextNode(child)); + } else { + element.appendChild(child); + } + } + + return element; +} + +const translateRe = /translate3d\((-?[0-9]+)px, (-?[0-9]+)px, 0px\)/; + +/** + * @param {number} x + * @param {number} y + */ +function getRootTransform(x, y) { + return `translate3d(${x}px, ${y}px, 0)`; +} + +function getInitialRootTransform() { + const x = window.innerWidth - 200 - 24; + const y = 24; + return getRootTransform(x, y); +} + +class MockFetchControl extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: "open" }); + + /** @type {import('./mockFetch').Config} */ + this._config = null; + this._show = false; + + const style = document.createElement("style"); + style.innerHTML = ` + #root { + display: none; + padding: 1rem 0.5rem; + border: 1px solid black; + border-radius: 8px; + background-color: white; + } + + #root.show { + display: block; + } + + #root.dialog { + position: fixed; + top: 0; + left: 0; + z-index: 1; + /* https://getcssscan.com/css-box-shadow-examples */ + box-shadow: rgba(0, 0, 0, 0.1) 0px 10px 15px -3px, rgba(0, 0, 0, 0.05) 0px 4px 6px -2px; + } + + button { + border: 0; + padding: 0; + background: none; + font-size: inherit; + font-family: -apple-system,system-ui,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,sans-serif; + } + + .drag-handle { + display: none; + position: absolute; + top: 0; + left: 0; + right: 0; + width: 100%; + height: 1.1rem; + text-align: center; + cursor: move; + border-radius: 8px 8px 0 0; + } + + .dialog .drag-handle { + display: block; + } + + .drag-handle:hover { + background-color: #ccc; + } + + .drag-handle-icon { + display: inline-block; + transform: rotate(90deg); + } + + label { + display: block; + } + + input#latency { + display: block; + } + + h2 { + font-size: 20px; + margin: 0; + margin-top: 0.5rem; + } + + ul { + margin: 0; + padding: 0; + list-style: none; + } + + ul:empty::after { + content: "(Empty)"; + display: block; + } + `; + this.shadowRoot.appendChild(style); + + const body = ( +
+ + + + +

Inflight

+
    +

    Recently done

    +
      +
      + ); + + this.shadowRoot.appendChild(body); + this.update(); + } + + get config() { + return this._config; + } + set config(newConfig) { + if (newConfig !== this._config) { + if (this._config) { + // Reset old config to 'auto' + this._config.mode = "auto"; + } + + newConfig.mode = "interactive"; + newConfig.on("update", () => this.update()); + this._config = newConfig; + } + } + + get show() { + return this.hasAttribute("show"); + } + set show(newShow) { + if (newShow) { + this.setAttribute("show", ""); + } else { + this.removeAttribute("show"); + } + } + + get dialog() { + return this.hasAttribute("dialog"); + } + set dialog(newDialog) { + if (newDialog) { + this.setAttribute("dialog", ""); + } else { + this.removeAttribute("dialog"); + } + } + + connectedCallback() { + this.update(); + } + + disconnectedCallback() { + this.update(); + } + + static get observedAttributes() { + return ["show", "dialog"]; + } + + attributeChangedCallback(name, oldValue, newValue) { + const root = this.shadowRoot.getElementById("root"); + + if (name == "show") { + if (newValue == null) { + root.classList.remove("show"); + } else { + root.classList.add("show"); + } + } + + if (name == "dialog") { + let match; + if (root.style.transform == "") { + root.style.transform = getInitialRootTransform(); + } else if ((match = root.style.transform.match(translateRe))) { + let translateX = match[1]; + let translateY = match[2]; + + if ( + translateX + 24 > window.innerWidth || + translateY + 24 > window.innerHeight + ) { + // If dialog is positioned off screen due to a screen resize, toggling + // the dialog should reset it's position + root.style.transform = getInitialRootTransform(); + } + } + + if (newValue == null) { + root.classList.remove("dialog"); + } else { + root.classList.add("dialog"); + } + } + + if (name == "show") { + this.update(); + } + } + + /** @param {MouseEvent} initialEvent */ + onInitializeMove(initialEvent) { + initialEvent.preventDefault(); + const root = this.shadowRoot.getElementById("root"); + let prevClientX = initialEvent.clientX; + let prevClientY = initialEvent.clientY; + let prevTranslateX = 0; + let prevTranslateY = 0; + + let match = root.style.transform.match(translateRe); + if (match) { + prevTranslateX = parseInt(match[1], 10); + prevTranslateY = parseInt(match[2], 10); + } + + /** @param {MouseEvent} moveEvent */ + const onMove = moveEvent => { + moveEvent.preventDefault(); + + let moveX = moveEvent.clientX - prevClientX; + let moveY = moveEvent.clientY - prevClientY; + + let newTranslateX = prevTranslateX + moveX; + let newTranslateY = prevTranslateY + moveY; + + if ( + // Outside bottom/right edge + moveEvent.clientX + 24 < window.innerWidth && + newTranslateY + 24 < window.innerHeight && + // Outside top/left edge + moveEvent.clientX - 24 > 0 && + newTranslateY > 0 + ) { + root.style.transform = getRootTransform(newTranslateX, newTranslateY); + } + + prevClientX = moveEvent.clientX; + prevClientY = moveEvent.clientY; + prevTranslateX = newTranslateX; + prevTranslateY = newTranslateY; + }; + + const onMoveEnd = () => { + document.removeEventListener("mousemove", onMove); + document.removeEventListener("mouseup", onMoveEnd); + }; + + document.addEventListener("mousemove", onMove); + document.addEventListener("mouseup", onMoveEnd); + } + + /** @param {InputEvent} event */ + onLatencyInput(event) { + this.config.durationMs = event.target.valueAsNumber; + this.update(); + } + + /** @param {Event} event */ + onPauseNew(event) { + this.config.areNewRequestsPaused = event.target.checked; + } + + update() { + if (!this.isConnected || this.show == false) { + // Should I check for display: none? + return; + } + + const latency = this.shadowRoot.getElementById("latency"); + latency.valueAsNumber = this.config.durationMs; + + const latencySec = (latency.valueAsNumber / 1000).toFixed(1); + const latencyLabel = this.shadowRoot.getElementById("latency-label"); + latencyLabel.textContent = `${latencySec} second${ + latencySec == "1.0" ? "" : "s" + }`; + } +} + +export function installFetchDebugger() { + window.customElements.define("mock-fetch-debugger", MockFetchControl); +} diff --git a/scripts/bundles/site.js b/scripts/bundles/site.js index 419ca6a..c31c3cc 100644 --- a/scripts/bundles/site.js +++ b/scripts/bundles/site.js @@ -3,13 +3,21 @@ import { setupToggle } from "./controls/toggle"; import { setupOffCanvas } from "./controls/offcanvas"; import { installPolyfill } from "./controls/details-polyfill"; import { createMockFetch, createMockFetchConfig } from "./controls/mockFetch"; +import { installFetchDebugger } from "./controls/mockFetchDebugger"; window.mockFetchConfig = createMockFetchConfig(); window.mockFetchConfig.log = (...msgs) => console.log(...msgs); window.mockFetch = createMockFetch(window.mockFetchConfig); +installFetchDebugger(); installPolyfill(); setupTabs(); setupToggle(); setupOffCanvas(); + +const fetchDebugger = document.createElement("mock-fetch-debugger"); +fetchDebugger.config = window.mockFetchConfig; +fetchDebugger.show = true; +fetchDebugger.dialog = true; +document.body.appendChild(fetchDebugger); diff --git a/scripts/bundles/site.scss b/scripts/bundles/site.scss index 0d805a6..8480587 100644 --- a/scripts/bundles/site.scss +++ b/scripts/bundles/site.scss @@ -266,3 +266,13 @@ App Page styles align-items: baseline; gap: 12px; } + +/******************************* + +Mock Fetch Debugger styles + +********************************/ + +mock-fetch-debugger { + display: block; +} diff --git a/tests/lib/mockFetch.test.js b/tests/lib/mockFetch.test.js index 4713a9a..3d2a3cc 100644 --- a/tests/lib/mockFetch.test.js +++ b/tests/lib/mockFetch.test.js @@ -615,6 +615,4 @@ describe("mockFetch library", () => { }); }); }); - - describe("interactive mode", () => {}); }); From cd239e5d045cbc02c2cbfccb2104afeeb4cbfd7c Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sat, 19 Dec 2020 13:22:19 -0800 Subject: [PATCH 05/28] Fix TS issues in site.js --- scripts/bundles/controls/mockFetch.d.ts | 6 ++++++ scripts/bundles/controls/mockFetchDebugger.js | 4 ++-- scripts/bundles/global.d.ts | 17 +++++++++++++++++ scripts/bundles/{controls => }/jsconfig.json | 4 +++- scripts/bundles/site.js | 3 +++ 5 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 scripts/bundles/global.d.ts rename scripts/bundles/{controls => }/jsconfig.json (65%) diff --git a/scripts/bundles/controls/mockFetch.d.ts b/scripts/bundles/controls/mockFetch.d.ts index d9c9eed..93b3ad0 100644 --- a/scripts/bundles/controls/mockFetch.d.ts +++ b/scripts/bundles/controls/mockFetch.d.ts @@ -39,6 +39,12 @@ interface Config { _emit(type: MockFetchEventType): void; } +declare class MockFetchDebugger extends HTMLElement { + config: Config; + show: boolean; + dialog: boolean; +} + export function createMockFetchConfig(): Config; export function createMockFetch( config: Config diff --git a/scripts/bundles/controls/mockFetchDebugger.js b/scripts/bundles/controls/mockFetchDebugger.js index 8666a46..29d248c 100644 --- a/scripts/bundles/controls/mockFetchDebugger.js +++ b/scripts/bundles/controls/mockFetchDebugger.js @@ -48,7 +48,7 @@ function getInitialRootTransform() { return getRootTransform(x, y); } -class MockFetchControl extends HTMLElement { +class MockFetchDebugger extends HTMLElement { constructor() { super(); this.attachShadow({ mode: "open" }); @@ -353,5 +353,5 @@ class MockFetchControl extends HTMLElement { } export function installFetchDebugger() { - window.customElements.define("mock-fetch-debugger", MockFetchControl); + window.customElements.define("mock-fetch-debugger", MockFetchDebugger); } diff --git a/scripts/bundles/global.d.ts b/scripts/bundles/global.d.ts new file mode 100644 index 0000000..a4fbf0b --- /dev/null +++ b/scripts/bundles/global.d.ts @@ -0,0 +1,17 @@ +import { + Config, + createMockFetch, + MockFetchDebugger +} from "./controls/mockFetch"; + +declare global { + interface HTMLElementTagNameMap { + "mock-fetch-debugger": MockFetchDebugger; + } + + interface Window { + mockFetchConfig: Config; + mockFetch: ReturnType; + fetchDebugger: MockFetchDebugger; + } +} diff --git a/scripts/bundles/controls/jsconfig.json b/scripts/bundles/jsconfig.json similarity index 65% rename from scripts/bundles/controls/jsconfig.json rename to scripts/bundles/jsconfig.json index 5280256..dcd7a26 100644 --- a/scripts/bundles/controls/jsconfig.json +++ b/scripts/bundles/jsconfig.json @@ -2,7 +2,9 @@ "compilerOptions": { "target": "es2017", "moduleResolution": "node", + "checkJs": true, "jsx": "react", "jsxFactory": "h" - } + }, + "include": ["**/*.js", "**/*.d.ts"] } diff --git a/scripts/bundles/site.js b/scripts/bundles/site.js index c31c3cc..0afbcfa 100644 --- a/scripts/bundles/site.js +++ b/scripts/bundles/site.js @@ -1,3 +1,5 @@ +/// + import { setupTabs } from "./controls/tabs"; import { setupToggle } from "./controls/toggle"; import { setupOffCanvas } from "./controls/offcanvas"; @@ -21,3 +23,4 @@ fetchDebugger.config = window.mockFetchConfig; fetchDebugger.show = true; fetchDebugger.dialog = true; document.body.appendChild(fetchDebugger); +window.fetchDebugger = fetchDebugger; From 153c7171f7b3f5c6626b7459ecce6aa97a918fdd Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sat, 19 Dec 2020 14:04:47 -0800 Subject: [PATCH 06/28] Fix initial attribute render & fix some more TS errors --- scripts/bundles/controls/jsx.d.ts | 14 ++++++++--- scripts/bundles/controls/mockFetchDebugger.js | 24 +++++++++++++++---- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/scripts/bundles/controls/jsx.d.ts b/scripts/bundles/controls/jsx.d.ts index d0267ae..aca1515 100644 --- a/scripts/bundles/controls/jsx.d.ts +++ b/scripts/bundles/controls/jsx.d.ts @@ -1,15 +1,23 @@ +type Children = undefined | string | JSX.Element | Array; + interface JSXAttributes { - children: HTMLElement[]; + children?: Children; + + // Attributes that this JSX allows + class?: string; + for?: string; } +type JSXHTMLElement = Partial>; + type HTMLElementsMap = { - [K in keyof HTMLElementTagNameMap]: HTMLElementTagNameMap[K] & JSXAttributes; + [K in keyof HTMLElementTagNameMap]: JSXHTMLElement & JSXAttributes; }; declare namespace JSX { interface Element extends HTMLElement {} interface ElementChildrenAttribute { - children: Array; + children?: Children; } interface IntrinsicElements extends HTMLElementsMap {} } diff --git a/scripts/bundles/controls/mockFetchDebugger.js b/scripts/bundles/controls/mockFetchDebugger.js index 29d248c..1eac2de 100644 --- a/scripts/bundles/controls/mockFetchDebugger.js +++ b/scripts/bundles/controls/mockFetchDebugger.js @@ -1,3 +1,7 @@ +/// + +import cc from "classcat"; + const hasOwn = Object.prototype.hasOwnProperty; /** @jsx h */ @@ -61,7 +65,7 @@ class MockFetchDebugger extends HTMLElement { style.innerHTML = ` #root { display: none; - padding: 1rem 0.5rem; + padding: 1.1rem 0.5rem; border: 1px solid black; border-radius: 8px; background-color: white; @@ -142,7 +146,13 @@ class MockFetchDebugger extends HTMLElement { this.shadowRoot.appendChild(style); const body = ( -
      +
      + + + ); + } + } + + // Move finished requests + /** @type {JSX.Element[]} */ + const finishedItems = []; + const completedList = this.shadowRoot.getElementById("completed"); + for (let request of finished) { + this.shadowRoot.querySelector(`[data-req-id="${request.id}"]`).remove(); + request.resolve(); + requests.delete(request.id); + + const newItem = ( + // @ts-expect-error +
    • event.target.remove()}>{request.url}
    • + ); + + finishedItems.push(newItem); + completedList.appendChild(newItem); + } + + requestAnimationFrame(() => { + finishedItems.forEach(li => (li.style.opacity = "0")); + this.update(); + }); } } diff --git a/tests/lib/mockFetchDebuggerTest.html b/tests/lib/mockFetchDebuggerTest.html index 540c251..4af7e7c 100644 --- a/tests/lib/mockFetchDebuggerTest.html +++ b/tests/lib/mockFetchDebuggerTest.html @@ -8,6 +8,15 @@ +
      + From 8d62ddb8cec1ae4ce85f30983ce11e60e37f180c Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sat, 19 Dec 2020 18:38:47 -0800 Subject: [PATCH 09/28] Convert mouse events to pointer events --- scripts/bundles/controls/mockFetchDebugger.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/bundles/controls/mockFetchDebugger.js b/scripts/bundles/controls/mockFetchDebugger.js index f257b1b..f27ff7f 100644 --- a/scripts/bundles/controls/mockFetchDebugger.js +++ b/scripts/bundles/controls/mockFetchDebugger.js @@ -215,7 +215,7 @@ class MockFetchDebugger extends HTMLElement { class="drag-handle" type="button" aria-label="Move fetch debugger" - onmousedown={this.onInitializeMove.bind(this)} + onpointerdown={this.onInitializeMove.bind(this)} >