@cocreate/position 1.16.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,307 @@
1
+ const { test, before, after } = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+ const http = require('node:http');
7
+ const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'playwright-core');
8
+
9
+ const root = path.resolve(__dirname, '..');
10
+ let directory, browser, server, origin;
11
+ before(async () => {
12
+ directory = fs.mkdtempSync(path.join(os.tmpdir(), 'position-auto-scroll-'));
13
+ const config = await require('../webpack.config.js')({}, { mode: 'development' });
14
+ config.context = root;
15
+ config.output.path = directory;
16
+ config.devtool = false;
17
+ config.plugins = config.plugins.filter(plugin => plugin.constructor.name !== 'FileUploader');
18
+ const compiler = require('webpack')(config);
19
+ try {
20
+ await new Promise((resolve, reject) => compiler.run((error, stats) => {
21
+ if (error) return reject(error);
22
+ if (stats.hasErrors()) return reject(new Error(stats.toString({ all: false, errors: true })));
23
+ resolve();
24
+ }));
25
+ } finally { await new Promise(resolve => compiler.close(resolve)); }
26
+ server = http.createServer((request, response) => {
27
+ const pathname = new URL(request.url, 'http://localhost').pathname;
28
+ if (pathname === '/') {
29
+ response.setHeader('Content-Type', 'text/html');
30
+ return response.end('<!doctype html><body></body>');
31
+ }
32
+ if (pathname === '/api/ping') return response.writeHead(204, { Date: new Date().toUTCString() }).end();
33
+ const file = path.resolve(directory, '.' + pathname);
34
+ if (!file.startsWith(directory + '/') || !fs.existsSync(file)) return response.writeHead(404).end();
35
+ response.setHeader('Content-Type', file.endsWith('.css') ? 'text/css' : 'application/javascript');
36
+ response.end(fs.readFileSync(file));
37
+ });
38
+ await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
39
+ origin = `http://127.0.0.1:${server.address().port}`;
40
+ browser = await chromium.launch({ headless: true, args: ['--no-sandbox'],
41
+ ...(process.env.CHROMIUM_PATH ? { executablePath: process.env.CHROMIUM_PATH } : {}) });
42
+ });
43
+ after(async () => {
44
+ await browser?.close();
45
+ if (server) {
46
+ server.closeAllConnections();
47
+ await new Promise(resolve => server.close(resolve));
48
+ }
49
+ if (directory) fs.rmSync(directory, { recursive: true, force: true });
50
+ });
51
+
52
+ async function fixture(run, { nested = false, clone = false, snap = false, empty = false } = {}) {
53
+ const page = await browser.newPage({ viewport: { width: 900, height: 700 } });
54
+ page.setDefaultTimeout(5000);
55
+ const errors = [], requests = [];
56
+ page.on('pageerror', error => errors.push(error.message));
57
+ page.on('response', response => {
58
+ if (response.status() >= 400) errors.push(`${response.status()} ${response.url()}`);
59
+ });
60
+ page.on('request', request => requests.push(new URL(request.url()).pathname));
61
+ try {
62
+ await page.goto(origin);
63
+ await page.evaluate(() => {
64
+ window.positionErrors = [];
65
+ document.addEventListener('position-error', event => positionErrors.push(event.detail.error.message), true);
66
+ });
67
+ await page.evaluate(({ nested, clone, snap, empty }) => {
68
+ document.body.style.margin = '0';
69
+ const scope = document.createElement('section');
70
+ scope.id = 'scope';
71
+ scope.setAttribute(clone ? 'clonable-query' : 'draggable-query', '.card[]');
72
+ scope.setAttribute(snap ? 'snappable-query' : 'droppable-query', '.drop[]');
73
+ scope.innerHTML = '<header style="height:36px"><div id="source" class="card" style="width:100px;height:36px;user-select:none">drag</div></header>';
74
+ const scroller = document.createElement('div');
75
+ scroller.id = 'scroller';
76
+ scroller.className = 'drop';
77
+ scroller.style.cssText = 'margin:40px 80px;width:260px;height:220px;overflow-y:auto;scroll-behavior:smooth';
78
+ for (let index = 0; index < (empty ? 0 : 30); index++) {
79
+ const item = document.createElement('div');
80
+ item.textContent = `item ${index}`;
81
+ item.style.height = '44px';
82
+ scroller.append(item);
83
+ }
84
+ if (nested) {
85
+ const outer = document.createElement('div');
86
+ outer.id = 'outer';
87
+ outer.style.cssText = 'margin:40px 80px;width:280px;height:220px;overflow-y:auto';
88
+ scroller.style.margin = '0';
89
+ outer.append(scroller);
90
+ const tail = document.createElement('div');
91
+ tail.style.height = '900px';
92
+ outer.append(tail);
93
+ scope.append(outer);
94
+ } else scope.append(scroller);
95
+ document.body.append(scope);
96
+ }, { nested, clone, empty, snap });
97
+ await page.addStyleTag({ url: origin + '/CoCreate-position.css' });
98
+ await page.addScriptTag({ url: origin + '/CoCreate-position.js' });
99
+ await page.waitForFunction(() => !!document.querySelector('#scope').positionDragDelegation);
100
+ await run(page, requests);
101
+ assert.deepEqual(errors, []);
102
+ assert.deepEqual(await page.evaluate(() => positionErrors), []);
103
+ } finally { await page.close(); }
104
+ }
105
+
106
+ async function startDrag(page) {
107
+ await page.locator('#source').hover();
108
+ await page.mouse.down();
109
+ await page.waitForFunction(() => document.querySelector('#source').position?.clickedInfo?.isMoving);
110
+ }
111
+ async function pointAt(page, edge, inset = 3) {
112
+ const box = await page.locator('#scroller').boundingBox();
113
+ await page.mouse.move(box.x + box.width / 2,
114
+ edge === 'top' ? box.y + inset : edge === 'middle' ? box.y + box.height / 2 : box.y + box.height - inset);
115
+ }
116
+ async function scrollTop(page) { return page.locator('#scroller').evaluate(node => node.scrollTop); }
117
+
118
+ test('a dragged item can snap back into its former container when it is the only child', () => fixture(async page => {
119
+ await startDrag(page);
120
+ const box = await page.locator('#scroller').boundingBox();
121
+ await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
122
+ await page.waitForFunction(() => {
123
+ const source = document.querySelector('#source');
124
+ return source.position.reference?.container === document.querySelector('#scroller');
125
+ });
126
+ await page.mouse.up();
127
+ await page.waitForFunction(() => {
128
+ const source = document.querySelector('#source');
129
+ return source.parentElement === document.querySelector('#scroller') && source.hasAttribute('snapped');
130
+ });
131
+
132
+ await startDrag(page);
133
+ await page.mouse.move(700, 600);
134
+ await page.waitForTimeout(60);
135
+ await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
136
+ await page.waitForFunction(() => {
137
+ const source = document.querySelector('#source');
138
+ return source.position.reference?.container === document.querySelector('#scroller');
139
+ });
140
+ await page.mouse.up();
141
+ await page.waitForFunction(() => {
142
+ const source = document.querySelector('#source');
143
+ return source.parentElement === document.querySelector('#scroller') && source.hasAttribute('snapped');
144
+ });
145
+ }, { snap: true, empty: true }));
146
+
147
+ // Real browser drag sessions exercise the module loader, engine, hit testing,
148
+ // scroll containers, and insertion references together.
149
+ test('auto-scroll loads only when a drag is active and continues under a stationary pointer', () => fixture(async (page, requests) => {
150
+ assert.equal(requests.some(name => name.includes('position-auto-scroll')), false);
151
+ await startDrag(page);
152
+ await pointAt(page, 'bottom');
153
+ await page.waitForFunction(() => document.querySelector('#scroller').scrollTop > 80);
154
+ assert.equal(requests.some(name => name.includes('position-auto-scroll')), true);
155
+ const first = await scrollTop(page);
156
+ await page.waitForFunction(first => document.querySelector('#scroller').scrollTop > first + 50, first);
157
+ assert.equal(await page.evaluate(() => {
158
+ const source = document.querySelector('#source');
159
+ const reference = source.position.reference;
160
+ return reference?.container === document.querySelector('#scroller') && reference.element.isConnected;
161
+ }), true);
162
+ await page.mouse.up();
163
+ await page.waitForFunction(() => !document.querySelector('#source').position.autoScroll);
164
+ const stopped = await scrollTop(page);
165
+ await page.waitForTimeout(100);
166
+ assert.equal(await scrollTop(page), stopped);
167
+ }));
168
+
169
+ test('scrolling stops when the pointer returns to the center', () => fixture(async page => {
170
+ await startDrag(page);
171
+ await pointAt(page, 'bottom');
172
+ await page.waitForFunction(() => document.querySelector('#scroller').scrollTop > 60);
173
+ await pointAt(page, 'middle');
174
+ await page.waitForTimeout(40);
175
+ const stopped = await scrollTop(page);
176
+ await page.waitForTimeout(150);
177
+ assert.equal(await scrollTop(page), stopped);
178
+ await page.mouse.up();
179
+ }));
180
+
181
+ test('the top edge scrolls upward', () => fixture(async page => {
182
+ await page.locator('#scroller').evaluate(node => node.scrollTo({ top: 400, behavior: 'instant' }));
183
+ await startDrag(page);
184
+ await pointAt(page, 'top');
185
+ await page.waitForFunction(() => document.querySelector('#scroller').scrollTop < 320);
186
+ await page.mouse.up();
187
+ }));
188
+
189
+ test('nested containers scroll the inner container before the outer container', () => fixture(async page => {
190
+ await startDrag(page);
191
+ await pointAt(page, 'bottom');
192
+ await page.waitForFunction(() => document.querySelector('#scroller').scrollTop > 60);
193
+ assert.equal(await page.locator('#outer').evaluate(node => node.scrollTop), 0);
194
+ await page.locator('#scroller').evaluate(node => node.scrollTo({ top: node.scrollHeight, behavior: 'instant' }));
195
+ await page.waitForFunction(() => document.querySelector('#outer').scrollTop > 30);
196
+ await page.mouse.up();
197
+ }, { nested: true }));
198
+
199
+ test('speed increases as the pointer approaches the edge', async () => {
200
+ const distances = [];
201
+ for (const inset of [35, 3]) {
202
+ await fixture(async page => {
203
+ await startDrag(page);
204
+ await pointAt(page, 'bottom', inset);
205
+ await page.waitForFunction(() => document.querySelector('#scroller').scrollTop > 0);
206
+ const before = await scrollTop(page);
207
+ await page.waitForTimeout(200);
208
+ distances.push(await scrollTop(page) - before);
209
+ await page.mouse.up();
210
+ });
211
+ }
212
+ assert.ok(distances[1] > distances[0] * 2, `expected faster scrolling near the edge: ${distances}`);
213
+ });
214
+
215
+ test('cloning retains its source while auto-scrolling toward a drop destination', () => fixture(async page => {
216
+ await startDrag(page);
217
+ await pointAt(page, 'bottom');
218
+ await page.waitForFunction(() => document.querySelector('#scroller').scrollTop > 60);
219
+ await page.mouse.up();
220
+ await page.waitForFunction(() => document.querySelector('#scroller .card'));
221
+ assert.equal(await page.locator('#scope > header > .card').count(), 1);
222
+ assert.equal(await page.locator('#scroller .card').count(), 1);
223
+ }, { clone: true }));
224
+
225
+ test('blur and source removal stop an active scroll loop', async () => {
226
+ for (const remove of [false, true]) await fixture(async page => {
227
+ await startDrag(page);
228
+ await pointAt(page, 'bottom');
229
+ await page.waitForFunction(() => document.querySelector('#scroller').scrollTop > 60);
230
+ await page.evaluate(remove => {
231
+ if (remove) document.querySelector('#source').remove();
232
+ else window.dispatchEvent(new Event('blur'));
233
+ }, remove);
234
+ await page.waitForTimeout(40);
235
+ const stopped = await scrollTop(page);
236
+ await page.waitForTimeout(150);
237
+ assert.equal(await scrollTop(page), stopped);
238
+ await page.mouse.up();
239
+ });
240
+ });
241
+
242
+ test('releasing a drag while the auto-scroll chunk loads cannot start a later session', () => fixture(async (page, requests) => {
243
+ let release;
244
+ const delayed = new Promise(resolve => { release = resolve; });
245
+ await page.route('**/position-auto-scroll.js', async route => { await delayed; await route.continue(); });
246
+ const requested = page.waitForRequest(request => request.url().includes('position-auto-scroll'));
247
+ try {
248
+ await startDrag(page);
249
+ await pointAt(page, 'bottom');
250
+ await requested;
251
+ assert.equal(requests.some(name => name.includes('position-auto-scroll')), true);
252
+ await page.mouse.up();
253
+ } finally { release(); }
254
+ await page.waitForTimeout(150);
255
+ assert.equal(await scrollTop(page), 0);
256
+ assert.equal(await page.evaluate(() => !!document.querySelector('#source').position.autoScroll), false);
257
+ }));
258
+
259
+ test('the viewport scrolls at its edge and respects a page scroll lock', async () => {
260
+ for (const locked of [false, true]) await fixture(async page => {
261
+ await page.evaluate(locked => {
262
+ document.body.style.minHeight = '2000px';
263
+ if (locked) document.body.style.overflow = 'hidden';
264
+ }, locked);
265
+ await startDrag(page);
266
+ await page.mouse.move(700, 697);
267
+ if (locked) {
268
+ await page.waitForTimeout(200);
269
+ assert.equal(await page.evaluate(() => document.scrollingElement.scrollTop), 0);
270
+ } else {
271
+ await page.waitForFunction(() => document.scrollingElement.scrollTop > 60);
272
+ }
273
+ await page.mouse.up();
274
+ });
275
+ });
276
+
277
+ for (const scale of [1, 0.75]) {
278
+ test(`same-origin iframe drop containers auto-scroll with frame scale ${scale}`, () => fixture(async page => {
279
+ await page.evaluate(async scale => {
280
+ const scope = document.querySelector('#scope');
281
+ scope.setAttribute('droppable-query', '#frame $contentDocument .drop');
282
+ document.querySelector('#scroller').remove();
283
+ window.dragSource = document.querySelector('#source');
284
+ const frame = document.createElement('iframe');
285
+ frame.id = 'frame';
286
+ frame.style.cssText = `margin:40px 80px;width:300px;height:260px;border:4px solid;transform:scale(${scale});transform-origin:top left`;
287
+ frame.srcdoc = '<!doctype html><style>body{margin:0}.drop{width:260px;height:220px;overflow-y:auto}.row{height:44px}</style><div class="drop">' +
288
+ Array.from({ length: 30 }, (_, index) => `<div class="row">frame item ${index}</div>`).join('') + '</div>';
289
+ const ready = new Promise(resolve => frame.onload = resolve);
290
+ scope.append(frame);
291
+ await ready;
292
+ }, scale);
293
+ await startDrag(page);
294
+ await page.mouse.move(150, 50);
295
+ const box = await page.frameLocator('#frame').locator('.drop').boundingBox();
296
+ const pointer = { x: box.x + box.width / 2, y: box.y + box.height - 3 };
297
+ await page.mouse.move(pointer.x, pointer.y);
298
+ await page.waitForFunction(() => document.querySelector('#frame').contentDocument.querySelector('.drop').scrollTop > 60);
299
+ assert.equal(await page.evaluate(() => dragSource.position.reference.document ===
300
+ document.querySelector('#frame').contentDocument), true);
301
+ const ghost = await page.locator('.position-moving-preview').boundingBox();
302
+ const offset = await page.evaluate(() => dragSource.position.clickedInfo.x);
303
+ assert.ok(Math.abs(ghost.x - (pointer.x - offset)) < 2, 'the preview must retain its pointer offset across frame coordinates');
304
+ await page.mouse.up();
305
+ assert.equal(await page.evaluate(() => !!dragSource.position.autoScroll), false);
306
+ }));
307
+ }
package/webpack.config.js CHANGED
@@ -48,6 +48,7 @@ module.exports = async (env, argv) => {
48
48
  minimizer: [
49
49
  new EsbuildPlugin({
50
50
  target: "es2017",
51
+ minify: true,
51
52
  css: true
52
53
  })
53
54
  ],