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>
272 lines
9.5 KiB
TypeScript
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' });
|
|
});
|
|
});
|