Files
smoothschedule/frontend/tests/e2e/scheduler-drag-past-8pm.spec.ts
poduck 2e111364a2 Initial commit: SmoothSchedule multi-tenant scheduling platform
This commit includes:
- Django backend with multi-tenancy (django-tenants)
- React + TypeScript frontend with Vite
- Platform administration API with role-based access control
- Authentication system with token-based auth
- Quick login dev tools for testing different user roles
- CORS and CSRF configuration for local development
- Docker development environment setup

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-27 01:43:20 -05:00

272 lines
9.5 KiB
TypeScript

/**
* Test: Appointment disappears when dragged past 8 PM
*
* This test reproduces the bug where dragging an appointment
* past 8 PM on the scheduler causes it to disappear.
*/
import { test, expect } from '@playwright/test';
test.describe('Scheduler Drag Past 8 PM Bug', () => {
test.beforeEach(async ({ page, context }) => {
// Clear cookies to avoid stale auth issues
await context.clearCookies();
// Login as acme business owner
await page.goto('http://acme.lvh.me:5173/login');
await page.fill('input[name="username"], input[placeholder*="username" i], input[type="text"]', 'acme_owner');
await page.fill('input[name="password"], input[placeholder*="password" i], input[type="password"]', 'password123');
await page.click('button[type="submit"]');
// Wait for dashboard to load
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
// Click on Scheduler in the sidebar
await page.getByRole('link', { name: 'Scheduler' }).click();
await page.waitForLoadState('networkidle');
await page.waitForTimeout(500);
});
test('should not disappear when dragging appointment past 8 PM', async ({ page }) => {
// Wait for scheduler to load
await page.waitForSelector('.timeline-scroll', { timeout: 10000 });
// Find an appointment on the scheduler
const appointments = page.locator('[draggable="true"]').filter({ hasText: /min/ });
const appointmentCount = await appointments.count();
console.log(`Found ${appointmentCount} appointments`);
if (appointmentCount === 0) {
test.skip('No appointments found to test with');
return;
}
// Get the first appointment
const appointment = appointments.first();
const appointmentText = await appointment.textContent();
console.log(`Testing with appointment: ${appointmentText}`);
// Get initial bounding box
const initialBox = await appointment.boundingBox();
if (!initialBox) {
test.skip('Could not get appointment bounding box');
return;
}
console.log(`Initial position: x=${initialBox.x}, y=${initialBox.y}`);
// Find the timeline container
const timeline = page.locator('.timeline-scroll');
const timelineBox = await timeline.boundingBox();
if (!timelineBox) {
throw new Error('Could not get timeline bounding box');
}
// Calculate 8 PM position (20 hours * 60 minutes * 2.5 pixels per minute = 3000 pixels from start)
// Plus we need to scroll and find the exact position
const pixelsPerMinute = 2.5;
const eightPMMinutes = 20 * 60; // 1200 minutes
const tenPMMinutes = 22 * 60; // 1320 minutes - drag past 8 PM
const targetX = tenPMMinutes * pixelsPerMinute; // 3300 pixels from midnight
console.log(`Target X position for 10 PM: ${targetX}`);
// First, scroll the timeline to show the 8 PM area
await timeline.evaluate((el, scrollTo) => {
el.scrollLeft = scrollTo - 500; // Scroll to show 8 PM area with some padding
}, targetX);
await page.waitForTimeout(500); // Wait for scroll to complete
// Get the scroll position
const scrollLeft = await timeline.evaluate(el => el.scrollLeft);
console.log(`Scrolled to: ${scrollLeft}`);
// Now drag the appointment to 10 PM position (past 8 PM)
// The target position relative to the viewport
const targetXViewport = timelineBox.x + (targetX - scrollLeft);
const targetY = initialBox.y + initialBox.height / 2;
console.log(`Dragging to viewport position: x=${targetXViewport}, y=${targetY}`);
// Perform the drag
await appointment.hover();
await page.mouse.down();
await page.mouse.move(targetXViewport, targetY, { steps: 20 });
await page.waitForTimeout(100);
await page.mouse.up();
// Wait for any updates
await page.waitForTimeout(1000);
// Take a screenshot for debugging
await page.screenshot({ path: 'scheduler-after-drag-to-10pm.png', fullPage: true });
// Check if the appointment still exists
// First, look for it in the timeline
const appointmentsAfterDrag = page.locator('[draggable="true"]').filter({ hasText: /min/ });
const countAfterDrag = await appointmentsAfterDrag.count();
console.log(`Appointments after drag: ${countAfterDrag}`);
// The appointment should still be visible (either in timeline or pending)
expect(countAfterDrag).toBeGreaterThanOrEqual(appointmentCount);
// Also check console for any errors
const consoleErrors: string[] = [];
page.on('console', msg => {
if (msg.type() === 'error') {
consoleErrors.push(msg.text());
}
});
// Check network requests for failed appointment updates
const failedRequests: string[] = [];
page.on('response', response => {
if (response.url().includes('/appointments/') && response.status() >= 400) {
failedRequests.push(`${response.url()} - ${response.status()}`);
}
});
await page.waitForTimeout(500);
if (consoleErrors.length > 0) {
console.log('Console errors:', consoleErrors);
}
if (failedRequests.length > 0) {
console.log('Failed requests:', failedRequests);
}
});
test('debug: log scheduler time range and appointments', async ({ page }) => {
// Wait for scheduler to load
await page.waitForSelector('.timeline-scroll', { timeout: 10000 });
// Take initial screenshot
await page.screenshot({ path: 'scheduler-initial.png', fullPage: true });
// Log timeline info
const timeline = page.locator('.timeline-scroll');
const timelineInfo = await timeline.evaluate(el => ({
scrollWidth: el.scrollWidth,
clientWidth: el.clientWidth,
scrollLeft: el.scrollLeft,
}));
console.log('Timeline info:', timelineInfo);
// Log all appointments
const appointments = page.locator('[draggable="true"]').filter({ hasText: /min/ });
const count = await appointments.count();
console.log(`Total appointments: ${count}`);
for (let i = 0; i < count; i++) {
const apt = appointments.nth(i);
const text = await apt.textContent();
const box = await apt.boundingBox();
console.log(`Appointment ${i}: "${text}" at position x=${box?.x}, y=${box?.y}, width=${box?.width}`);
}
// Scroll to different times and check visibility
const timesToCheck = [
{ hour: 8, name: '8 AM' },
{ hour: 12, name: '12 PM' },
{ hour: 17, name: '5 PM' },
{ hour: 20, name: '8 PM' },
{ hour: 22, name: '10 PM' },
];
for (const { hour, name } of timesToCheck) {
const scrollPosition = hour * 60 * 2.5 - 200; // pixels from midnight, minus padding
await timeline.evaluate((el, pos) => { el.scrollLeft = pos; }, scrollPosition);
await page.waitForTimeout(300);
const visibleApts = await appointments.count();
console.log(`At ${name}: ${visibleApts} appointments visible, scrollLeft=${await timeline.evaluate(el => el.scrollLeft)}`);
await page.screenshot({ path: `scheduler-at-${hour}h.png` });
}
});
test('debug: monitor network during drag operation', async ({ page }) => {
// Set up network logging
const networkLog: { method: string; url: string; status?: number; body?: any }[] = [];
page.on('request', request => {
if (request.url().includes('/api/')) {
networkLog.push({
method: request.method(),
url: request.url(),
body: request.postData() ? JSON.parse(request.postData() || '{}') : null,
});
}
});
page.on('response', async response => {
if (response.url().includes('/api/')) {
const entry = networkLog.find(e => e.url === response.url() && !e.status);
if (entry) {
entry.status = response.status();
}
}
});
// Wait for scheduler
await page.waitForSelector('.timeline-scroll', { timeout: 10000 });
await page.waitForTimeout(1000);
// Clear log after initial load
networkLog.length = 0;
// Find first appointment
const appointments = page.locator('[draggable="true"]').filter({ hasText: /min/ });
const appointment = appointments.first();
if (await appointment.count() === 0) {
test.skip('No appointments found');
return;
}
const initialBox = await appointment.boundingBox();
if (!initialBox) return;
// Scroll to 8 PM area
const timeline = page.locator('.timeline-scroll');
await timeline.evaluate(el => { el.scrollLeft = 2800; }); // Near 8 PM
await page.waitForTimeout(500);
const timelineBox = await timeline.boundingBox();
if (!timelineBox) return;
// Drag to 9 PM (21 * 60 * 2.5 = 3150, minus scroll)
const scrollLeft = await timeline.evaluate(el => el.scrollLeft);
const targetX = timelineBox.x + (3150 - scrollLeft);
console.log('Starting drag to 9 PM...');
await appointment.hover();
await page.mouse.down();
await page.mouse.move(targetX, initialBox.y + 50, { steps: 30 });
await page.waitForTimeout(200);
await page.mouse.up();
// Wait for network requests to complete
await page.waitForTimeout(2000);
console.log('Network requests during drag:', JSON.stringify(networkLog, null, 2));
// Check if there were any PATCH requests and their responses
const patchRequests = networkLog.filter(r => r.method === 'PATCH');
console.log(`PATCH requests: ${patchRequests.length}`);
for (const req of patchRequests) {
console.log(`PATCH ${req.url}: status=${req.status}, body=${JSON.stringify(req.body)}`);
}
await page.screenshot({ path: 'scheduler-after-drag-debug.png' });
});
});