diff --git a/README.md b/README.md index 560227c..5a2c8ed 100644 --- a/README.md +++ b/README.md @@ -166,13 +166,15 @@ integration tests. Build testing consists of linting the source code using `tsc` auto-formatting it using `prettier`, and compiling it into three bundles (debug, normal, and minified) using `esbuild`. Integration tests are found in the `tests/touch.spec.js` file, using Playwright as test runner. +Additionally, `npm run test:debug` will run the tests with `DEBUG` statements preserved, useful for when tests fail to pass and you're trying to find out what's actually happening. + ### Manual testing To manually test in the browser, you can run `npm start` and then open the URL that is printed to the console once the initial build tasks have finished. This runs a local server that lets you run the demo page, but with the `drag-drop-touch.esm.min.js` replaced by a `drag-drop-touch.debug.esm.js` -instead, which preserves all debug statements used in the TypeScript source. +instead, which preserves all debug statements used in the TypeScript source. To add your own debug statements, use the `DEBUG:` label followed by either a normal statement, or multiple statements wrapped in a new block. diff --git a/demo/index.html b/demo/index.html index 690e1d9..44f2666 100644 --- a/demo/index.html +++ b/demo/index.html @@ -4,10 +4,7 @@ DragDropTouch - + diff --git a/demo/index.js b/demo/index.js index a375078..c6d121b 100644 --- a/demo/index.js +++ b/demo/index.js @@ -1,10 +1,4 @@ -// Add a tiny touch simulator for CI testing -import { drag, tap } from "./touch-simulation.js"; -globalThis.simulatedTouch = { drag, tap }; - -// The rest of the code doesn't know anything about touch -// events, it's written as normal drag-and-drop handlers. -let draggable = null; +let draggable = null; const cols = document.querySelectorAll(`#columns .column`); cols.forEach((col) => { diff --git a/demo/issue-77b/index.html b/demo/issue-77b/index.html deleted file mode 100644 index bef2f09..0000000 --- a/demo/issue-77b/index.html +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - - - - - -

window A

- -

- - - - - testing - - - -

- -

window B

- - diff --git a/package.json b/package.json index e2e6d05..1c04580 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,9 @@ "server": "node server.js", "start": "npm run build && npm run server", "test:integration": "playwright test tests --workers=1 --timeout 5000 --global-timeout 15000", - "test": "npm run start -- -- --test && rm -rf test-results" + "test": "npm run start -- -- --test && npm run test:cleanup", + "test:debug": "npm run start -- -- --test --debug && npm run test:cleanup", + "test:cleanup": "rm -rf test-results" }, "repository": { "type": "git", diff --git a/server.js b/server.js index ce46535..8610436 100644 --- a/server.js +++ b/server.js @@ -9,6 +9,8 @@ process.env.PORT = PORT; const HOSTNAME = process.env.HOSTNAME ?? `localhost`; process.env.HOSTNAME = HOSTNAME; +const testing = process.argv.includes(`--test`); +const debug = process.argv.includes(`--debug`); const npm = process.platform === `win32` ? `npm.cmd` : `npm`; // Set up the core server @@ -25,7 +27,7 @@ app.use((req, res, next) => { app.set("etag", false); app.use((req, res, next) => { - if (!process.argv.includes(`--test`)) { + if (!testing || (testing && debug)) { console.log(`[${new Date().toISOString()}] ${req.url}`); } next(); @@ -35,11 +37,14 @@ app.use((req, res, next) => { app.use(`/`, (req, res, next) => { const { url } = req; if (url === `/`) { + if (testing) return res.redirect(`/tests/integration`); return res.redirect(`/demo`); } - const testing = process.argv.includes(`--test`); - if (url === `/dist/drag-drop-touch.esm.min.js?autoload` && !testing) { - return res.redirect(`/dist/drag-drop-touch.debug.esm.js?autoload`); + if ( + url.includes(`/dist/drag-drop-touch.esm.min.js`) && + (!testing || (testing && debug)) + ) { + return res.redirect(url.replace(`esm.min.js`, `debug.esm.js`)); } next(); }); @@ -61,7 +66,7 @@ app.listen(PORT, () => { console.log([``, line, mid, msg, mid, line, ``].join(`\n`)); // are we running tests? - if (process.argv.includes(`--test`)) { + if (testing) { const runner = spawn(npm, [`run`, `test:integration`], { stdio: `inherit`, }); diff --git a/tests/integration/index.css b/tests/integration/index.css new file mode 100644 index 0000000..46b84b7 --- /dev/null +++ b/tests/integration/index.css @@ -0,0 +1,46 @@ +[draggable] { + user-select: none; +} + +.dragging { + opacity: 0.5; +} + +#columns { + display: flex; + gap: 1rem; + + .column { + display: inline-block; + height: 150px; + width: 150px; + background: lightgrey; + opacity: 1; + + header { + color: #fff; + padding: 5px; + background: #222; + pointer-events: none; + } + + &.over { + border: 2px dashed #000; + opacity: 0.5; + box-sizing: border-box; + } + + & > input, + & > textarea, + & > select { + display: block; + width: 85%; + margin: 5%; + max-height: calc(1.25em * 4); + } + + & > img { + height: 50%; + } + } +} diff --git a/demo/issue-77/index.html b/tests/integration/index.html similarity index 71% rename from demo/issue-77/index.html rename to tests/integration/index.html index 9a8b67a..1c022cc 100644 --- a/demo/issue-77/index.html +++ b/tests/integration/index.html @@ -3,30 +3,31 @@ - Test file for issue 77 - - - + DragDropTouch + + +
-

Test page for issue 77

-

- https://github.com/drag-drop-touch-js/dragdroptouch/issues/77 -

+

TEST

+
+

A box dragging example

+

+ Drag some boxes around with the mouse, then open your Developer + Tools, turn on mobile emulation, and try to do the same with touch + input enabled. Things should still work. +

+
+
Input
diff --git a/tests/integration/index.js b/tests/integration/index.js new file mode 100644 index 0000000..a4b5006 --- /dev/null +++ b/tests/integration/index.js @@ -0,0 +1,81 @@ +// Add a tiny touch simulator for CI testing +import * as simulatedTouch from "./touch-simulation.js"; +globalThis.simulatedTouch = simulatedTouch; + +let draggable = null; +const cols = document.querySelectorAll(`#columns .column`); + +cols.forEach((col) => { + col.addEventListener(`dragstart`, handleDragStart); + col.addEventListener(`dragenter`, handleDragEnter); + col.addEventListener(`dragover`, handleDragOver); + col.addEventListener(`dragleave`, handleDragLeave); + col.addEventListener(`drop`, handleDrop); + col.addEventListener(`dragend`, handleDragEnd); +}); + +function handleDragStart({ target, dataTransfer }) { + if (target.className.includes(`column`)) { + draggable = target; + draggable.classList.add(`dragging`); + + dataTransfer.effectAllowed = `move`; + dataTransfer.setData(`text`, draggable.innerHTML); + + // customize drag image for one of the panels + const haveDragFn = dataTransfer.setDragImage instanceof Function; + if (haveDragFn && target.textContent.includes(`X`)) { + let img = new Image(); + img.src = `dragimage.jpg`; + dataTransfer.setDragImage(img, img.width / 2, img.height / 2); + } + } +} + +function handleDragOver(evt) { + if (draggable) { + evt.preventDefault(); + evt.dataTransfer.dropEffect = `move`; + } +} + +function handleDragEnter({ target }) { + if (draggable) { + target.classList.add(`over`); + } +} + +function handleDragLeave({ target }) { + if (draggable) { + target.classList.remove(`over`); + } +} + +function handleDragEnd() { + draggable = null; + cols.forEach((col) => col.classList.remove(`over`)); +} + +function handleDrop(evt) { + if (draggable === null) return; + + evt.stopPropagation(); + evt.stopImmediatePropagation(); + evt.preventDefault(); + + if (draggable !== this) { + swapDom(draggable, this); + } +} + +// https://stackoverflow.com/questions/9732624/how-to-swap-dom-child-nodes-in-javascript +function swapDom(a, b) { + let aParent = a.parentNode; + let bParent = b.parentNode; + let aHolder = document.createElement(`div`); + let bHolder = document.createElement(`div`); + aParent.replaceChild(aHolder, a); + bParent.replaceChild(bHolder, b); + aParent.replaceChild(b, aHolder); + bParent.replaceChild(a, bHolder); +} diff --git a/tests/integration/issue-77b/index.html b/tests/integration/issue-77b/index.html new file mode 100644 index 0000000..2670971 --- /dev/null +++ b/tests/integration/issue-77b/index.html @@ -0,0 +1,32 @@ + + + + + + + +

window A

+ +

+ + + + + testing + + + +

+ +

window B

+ +

+ + diff --git a/tests/integration/issue-77b/test.js b/tests/integration/issue-77b/test.js new file mode 100644 index 0000000..71b62ae --- /dev/null +++ b/tests/integration/issue-77b/test.js @@ -0,0 +1,23 @@ +import { enableDragDropTouch } from "../../../dist/drag-drop-touch.esm.min.js"; + +import * as simulatedTouch from "../touch-simulation.js"; +globalThis.simulatedTouch = simulatedTouch; + +globalThis.enablePressHold = (threshold = 0) => { + enableDragDropTouch(document, document, { + isPressHoldMode: true, + pressHoldThresholdPixels: threshold, + }); +}; + +document.addEventListener(`dragover`, (e) => e.preventDefault()); + +document.addEventListener(`drop`, (e) => { + e.preventDefault(); + document.getElementById(`result`).textContent = + e.dataTransfer.getData(`text/plain`); +}); + +document.addEventListener(`dragstart`, (e) => + e.dataTransfer.setData(`text/plain`, `we dragged a ${e.target.tagName}`) +); diff --git a/demo/touch-simulation.js b/tests/integration/touch-simulation.js similarity index 86% rename from demo/touch-simulation.js rename to tests/integration/touch-simulation.js index 4327582..755b4f6 100644 --- a/demo/touch-simulation.js +++ b/tests/integration/touch-simulation.js @@ -21,11 +21,13 @@ function simulate(eventType, element, { x, y }) { element.dispatchEvent(event); } -export /* async */ function drag(from, to) { +export /* async */ function drag(from, to, options = {}) { const { left, width, top } = from.getBoundingClientRect(); const touch = { x: left + width / 2, y: top + 1, target: from }; simulate("touchstart", from, touch); + const timeout = options.dragDelay || false; + // simulate a dragging track const steps = 10; const [dx, dy] = (function (l, t) { @@ -34,17 +36,20 @@ export /* async */ function drag(from, to) { })(left, top); return new Promise((resolve) => { - (function drag(i = 0) { + function drag(i = 0) { if (i === steps - 1) { simulate("touchend", to, touch); - setTimeout(resolve, 10); + return setTimeout(resolve, 10); } touch.x += dx; touch.y += dy; touch.target = document; simulate("touchmove", to, touch); setTimeout(() => drag(i + 1), 100 / steps); - })(); + } + + if (!timeout) drag(); + else setTimeout(drag, timeout); }); } diff --git a/tests/touch.spec.js b/tests/touch.spec.js index e3a5df5..50a4b40 100644 --- a/tests/touch.spec.js +++ b/tests/touch.spec.js @@ -4,14 +4,19 @@ async function bootstrapPage(browser, options = {}) { const context = await browser.newContext(options); const page = await context.newPage(); page.on("console", (msg) => console.log(msg.text())); - await page.goto(`http://localhost:8000`); + if (options.page) { + await page.goto(`http://localhost:8000/${options.page}`); + } else { + await page.goto(`http://localhost:8000`); + } return page; } test.describe(`touch events`, () => { - let page; + let browser, page; - test.beforeEach(async ({ browser }) => { + test.beforeEach(async ({ browser: b }) => { + browser = b; page = await bootstrapPage(browser, { hasTouch: true, }); @@ -21,7 +26,6 @@ test.describe(`touch events`, () => { return page.evaluate((eventType) => { return new Promise((resolve) => { document.querySelector(qs).addEventListener(eventType, ({ type }) => { - // fill this it resolve({ type }); }); }); @@ -38,14 +42,14 @@ test.describe(`touch events`, () => { ); } - async function touchDragEntry(sourceSelector, targetSelector) { + async function touchDragEntry(sourceSelector, targetSelector, options) { await page.evaluate( - async ({ sourceSelector, targetSelector }) => { + async ({ sourceSelector, targetSelector, options }) => { const from = document.querySelector(sourceSelector); const to = document.querySelector(targetSelector); - await globalThis.simulatedTouch.drag(from, to); + await globalThis.simulatedTouch.drag(from, to, options); }, - { sourceSelector, targetSelector } + { sourceSelector, targetSelector, options} ); } @@ -98,4 +102,38 @@ test.describe(`touch events`, () => { expect(await e1.textContent()).toBe(`Input`); expect(await e2.textContent()).toBe(`Image`); }); + + test(`longpress with 0px drag threshold`, async () => { + page = await bootstrapPage(browser, { + hasTouch: true, + page: `tests/integration/issue-77b/index.html`, + }); + + const textContent = await page.locator(`text`).textContent(); + expect(textContent.trim()).toBe(`testing`); + + await page.evaluate(() => globalThis.enablePressHold()); + const from = `#from`; + const to = `#to`; + await touchDragEntry(from, to, { dragDelay: 500 }); + + expect(await page.locator(`#result`).textContent()).toBe(`we dragged a A`); + }); + + test(`longpress with 25px drag threshold`, async () => { + page = await bootstrapPage(browser, { + hasTouch: true, + page: `tests/integration/issue-77b/index.html`, + }); + + const textContent = await page.locator(`text`).textContent(); + expect(textContent.trim()).toBe(`testing`); + + await page.evaluate(() => globalThis.enablePressHold(25)); + const from = `#from`; + const to = `#to`; + await touchDragEntry(from, to, { dragDelay: 500 }); + + expect(await page.locator(`#result`).textContent()).toBe(`we dragged a A`); + }); }); diff --git a/ts/drag-drop-touch.ts b/ts/drag-drop-touch.ts index 4e7cdfa..b7e7a15 100644 --- a/ts/drag-drop-touch.ts +++ b/ts/drag-drop-touch.ts @@ -229,8 +229,13 @@ class DragDropTouch { }, this.configuration.contextMenuDelayMS); if (this.configuration.isPressHoldMode) { - DEBUG: console.log(`setting a press-hold timeout`); + DEBUG: console.log( + `setting a press-hold timeout for ${this.configuration.pressHoldDelayMS}ms`, + ); this._pressHoldIntervalId = setTimeout(() => { + DEBUG: console.log( + `this._isDragEnabled = true, calling touchMove`, + ); this._isDragEnabled = true; this._touchmove(e); }, this.configuration.pressHoldDelayMS); @@ -399,6 +404,14 @@ class DragDropTouch { * @returns */ _shouldCancelPressHoldMove(e: TouchEvent) { + DEBUG: { + console.log({ + isPressHoldMode: this.configuration.isPressHoldMode, + _isDragEnabled: this._isDragEnabled, + delta: this._getDelta(e), + pressHoldMargin: this.configuration.pressHoldMargin, + }); + } return ( this.configuration.isPressHoldMode && !this._isDragEnabled &&