Mega push vol 6, started adding many awesome stuff, chat broken rn
This commit is contained in:
174
tests/e2e/kanban-perf.spec.ts
Normal file
174
tests/e2e/kanban-perf.spec.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { login, TEST_ORG_SLUG } from './helpers';
|
||||
|
||||
/**
|
||||
* Measures the REAL end-to-end latency a user experiences when dragging
|
||||
* a kanban card from one column to another using native mouse events.
|
||||
*
|
||||
* Uses MutationObserver set up BEFORE the drag starts, then performs
|
||||
* a real mouse drag-and-drop sequence. Measures from mouse-up to
|
||||
* card appearing in the target column DOM.
|
||||
*/
|
||||
test.describe('Kanban card move latency', () => {
|
||||
test('real drag-drop: card should appear in target column fast', async ({ page }) => {
|
||||
await login(page);
|
||||
await page.goto(`/${TEST_ORG_SLUG}/kanban`, { waitUntil: 'networkidle' });
|
||||
|
||||
// Click into the first board if we're on the board selector
|
||||
const anyBoard = page.locator('text=/board/i').first();
|
||||
if (await anyBoard.isVisible({ timeout: 3000 }).catch(() => false)) {
|
||||
await anyBoard.click();
|
||||
}
|
||||
|
||||
// Wait for columns
|
||||
await page.waitForSelector('[data-column-id]', { timeout: 10000 });
|
||||
|
||||
// Ensure there's a card to move — check first column
|
||||
const firstCard = page.locator('[data-column-id]').first().locator('[data-card-id]').first();
|
||||
if (!(await firstCard.isVisible({ timeout: 2000 }).catch(() => false))) {
|
||||
const addBtn = page.locator('[data-column-id]').first().getByRole('button', { name: /add card/i });
|
||||
if (await addBtn.isVisible({ timeout: 2000 }).catch(() => false)) {
|
||||
await addBtn.click();
|
||||
await page.waitForTimeout(300);
|
||||
const titleInput = page.locator('input[placeholder="Card title"]');
|
||||
await titleInput.fill('Perf Test Card');
|
||||
await page.getByRole('button', { name: 'Add Card', exact: true }).click();
|
||||
await page.waitForSelector('[data-card-id]', { timeout: 5000 });
|
||||
} else {
|
||||
console.log('Cannot create card, skipping');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Gather IDs
|
||||
const cardId = await page.locator('[data-column-id]').first().locator('[data-card-id]').first().getAttribute('data-card-id');
|
||||
const columns = page.locator('[data-column-id]');
|
||||
if ((await columns.count()) < 2) { console.log('Need 2+ columns'); return; }
|
||||
const dstColId = await columns.nth(1).getAttribute('data-column-id');
|
||||
|
||||
console.log(`Card: ${cardId}`);
|
||||
console.log(`Target column: ${dstColId}`);
|
||||
|
||||
// Set up a MutationObserver on the target column BEFORE the drag starts
|
||||
// This will record the exact timestamp when the card DOM node appears
|
||||
await page.evaluate(({ cardId, dstColId }) => {
|
||||
const dstCol = document.querySelector(`[data-column-id="${dstColId}"]`);
|
||||
if (!dstCol) return;
|
||||
|
||||
(window as any).__cardAppearedAt = 0;
|
||||
(window as any).__mouseUpAt = 0;
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
if ((window as any).__cardAppearedAt > 0) return;
|
||||
const found = dstCol.querySelector(`[data-card-id="${cardId}"]`);
|
||||
if (found) {
|
||||
(window as any).__cardAppearedAt = performance.now();
|
||||
observer.disconnect();
|
||||
}
|
||||
});
|
||||
observer.observe(dstCol, { childList: true, subtree: true });
|
||||
|
||||
// Also hook into mouseup to record the exact drop moment
|
||||
document.addEventListener('mouseup', () => {
|
||||
if ((window as any).__mouseUpAt === 0) {
|
||||
(window as any).__mouseUpAt = performance.now();
|
||||
}
|
||||
}, { once: true, capture: true });
|
||||
}, { cardId, dstColId });
|
||||
|
||||
// Get bounding boxes
|
||||
const card = page.locator('[data-column-id]').first().locator('[data-card-id]').first();
|
||||
const cardBox = await card.boundingBox();
|
||||
const targetBox = await columns.nth(1).boundingBox();
|
||||
if (!cardBox || !targetBox) { console.log('No bounding boxes'); return; }
|
||||
|
||||
const srcX = cardBox.x + cardBox.width / 2;
|
||||
const srcY = cardBox.y + cardBox.height / 2;
|
||||
const dstX = targetBox.x + targetBox.width / 2;
|
||||
const dstY = targetBox.y + 100;
|
||||
|
||||
// Perform REAL mouse drag
|
||||
await page.mouse.move(srcX, srcY);
|
||||
await page.mouse.down();
|
||||
// Small move to trigger dragstart
|
||||
await page.mouse.move(srcX + 5, srcY, { steps: 2 });
|
||||
await page.waitForTimeout(50); // Let browser register the drag
|
||||
// Move to target
|
||||
await page.mouse.move(dstX, dstY, { steps: 3 });
|
||||
await page.waitForTimeout(50); // Let dragover register
|
||||
// Drop
|
||||
await page.mouse.up();
|
||||
|
||||
// Wait a bit for everything to settle
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
// Read the timestamps
|
||||
const result = await page.evaluate(() => {
|
||||
return {
|
||||
mouseUpAt: (window as any).__mouseUpAt as number,
|
||||
cardAppearedAt: (window as any).__cardAppearedAt as number,
|
||||
};
|
||||
});
|
||||
|
||||
const dropToRender = result.cardAppearedAt > 0 && result.mouseUpAt > 0
|
||||
? result.cardAppearedAt - result.mouseUpAt
|
||||
: -1;
|
||||
|
||||
console.log(`\n========================================`);
|
||||
console.log(` mouseup timestamp: ${result.mouseUpAt.toFixed(1)}`);
|
||||
console.log(` card appeared at: ${result.cardAppearedAt.toFixed(1)}`);
|
||||
console.log(` DROP → RENDER LATENCY: ${dropToRender.toFixed(1)}ms`);
|
||||
if (dropToRender < 0) {
|
||||
console.log(' ⚠️ Card never appeared or mouseup not captured');
|
||||
console.log(' (HTML5 drag may not have fired — trying synthetic fallback)');
|
||||
} else if (dropToRender < 20) {
|
||||
console.log(' ✅ INSTANT (<20ms)');
|
||||
} else if (dropToRender < 50) {
|
||||
console.log(' ✅ VERY FAST (<50ms)');
|
||||
} else if (dropToRender < 100) {
|
||||
console.log(' ⚠️ PERCEPTIBLE (50-100ms)');
|
||||
} else if (dropToRender < 500) {
|
||||
console.log(' ❌ SLOW (100-500ms)');
|
||||
} else {
|
||||
console.log(' ❌ VERY SLOW (>500ms)');
|
||||
}
|
||||
console.log(`========================================\n`);
|
||||
|
||||
// If native drag didn't work, fall back to synthetic events to at least measure Svelte
|
||||
if (dropToRender < 0) {
|
||||
console.log('Falling back to synthetic drag events...');
|
||||
const synthLatency = await page.evaluate(({ cardId, dstColId }) => {
|
||||
return new Promise<number>((resolve) => {
|
||||
const cardEl = document.querySelector(`[data-card-id="${cardId}"]`);
|
||||
const dstCol = document.querySelector(`[data-column-id="${dstColId}"]`);
|
||||
if (!cardEl || !dstCol) { resolve(-1); return; }
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
const found = dstCol.querySelector(`[data-card-id="${cardId}"]`);
|
||||
if (found) { observer.disconnect(); resolve(performance.now() - t0); }
|
||||
});
|
||||
observer.observe(dstCol, { childList: true, subtree: true });
|
||||
|
||||
const dt = new DataTransfer();
|
||||
dt.setData('text/plain', cardId!);
|
||||
cardEl.dispatchEvent(new DragEvent('dragstart', { bubbles: true, cancelable: true, dataTransfer: dt }));
|
||||
dstCol.dispatchEvent(new DragEvent('dragover', { bubbles: true, cancelable: true, dataTransfer: dt }));
|
||||
const t0 = performance.now();
|
||||
dstCol.dispatchEvent(new DragEvent('drop', { bubbles: true, cancelable: true, dataTransfer: dt }));
|
||||
|
||||
setTimeout(() => {
|
||||
observer.disconnect();
|
||||
const found = dstCol.querySelector(`[data-card-id="${cardId}"]`);
|
||||
resolve(found ? performance.now() - t0 : 9999);
|
||||
}, 2000);
|
||||
});
|
||||
}, { cardId, dstColId });
|
||||
console.log(` Synthetic drop→render: ${synthLatency.toFixed(1)}ms`);
|
||||
}
|
||||
|
||||
// Verify card moved
|
||||
const cardInTarget = columns.nth(1).locator(`[data-card-id="${cardId}"]`);
|
||||
const visible = await cardInTarget.isVisible({ timeout: 3000 }).catch(() => false);
|
||||
console.log(` Card visible in target: ${visible}`);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user