Enhances event layout engine with advanced rendering logic
Introduces sophisticated event layout algorithm for handling complex scheduling scenarios Adds support for: - Grid and stacked event rendering - Automatic column allocation - Nested event stacking - Threshold-based event grouping Improves visual representation of overlapping and concurrent events
This commit is contained in:
parent
4e22fbc948
commit
70172e8f10
26 changed files with 2108 additions and 44 deletions
323
.workbench/scenarios/v2-scenario-renderer.js
Normal file
323
.workbench/scenarios/v2-scenario-renderer.js
Normal file
|
|
@ -0,0 +1,323 @@
|
|||
/**
|
||||
* V2 Scenario Renderer
|
||||
* Uses EventLayoutEngine from V2 to render events dynamically
|
||||
*/
|
||||
|
||||
// Import the compiled V2 layout engine
|
||||
// We'll inline the algorithm here for standalone use in browser
|
||||
|
||||
// ============================================
|
||||
// EventLayoutEngine (copied from V2 for browser use)
|
||||
// ============================================
|
||||
|
||||
function eventsOverlap(a, b) {
|
||||
return a.start < b.end && a.end > b.start;
|
||||
}
|
||||
|
||||
function eventsWithinThreshold(a, b, thresholdMinutes) {
|
||||
const thresholdMs = thresholdMinutes * 60 * 1000;
|
||||
|
||||
const startToStartDiff = Math.abs(a.start.getTime() - b.start.getTime());
|
||||
if (startToStartDiff <= thresholdMs) return true;
|
||||
|
||||
const bStartsBeforeAEnds = a.end.getTime() - b.start.getTime();
|
||||
if (bStartsBeforeAEnds > 0 && bStartsBeforeAEnds <= thresholdMs) return true;
|
||||
|
||||
const aStartsBeforeBEnds = b.end.getTime() - a.start.getTime();
|
||||
if (aStartsBeforeBEnds > 0 && aStartsBeforeBEnds <= thresholdMs) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function findOverlapGroups(events) {
|
||||
if (events.length === 0) return [];
|
||||
|
||||
const sorted = [...events].sort((a, b) => a.start.getTime() - b.start.getTime());
|
||||
const used = new Set();
|
||||
const groups = [];
|
||||
|
||||
for (const event of sorted) {
|
||||
if (used.has(event.id)) continue;
|
||||
|
||||
const group = [event];
|
||||
used.add(event.id);
|
||||
|
||||
let expanded = true;
|
||||
while (expanded) {
|
||||
expanded = false;
|
||||
for (const candidate of sorted) {
|
||||
if (used.has(candidate.id)) continue;
|
||||
|
||||
const connects = group.some(member => eventsOverlap(member, candidate));
|
||||
|
||||
if (connects) {
|
||||
group.push(candidate);
|
||||
used.add(candidate.id);
|
||||
expanded = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
groups.push(group);
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
function findGridCandidates(events, thresholdMinutes) {
|
||||
if (events.length === 0) return [];
|
||||
|
||||
const sorted = [...events].sort((a, b) => a.start.getTime() - b.start.getTime());
|
||||
const used = new Set();
|
||||
const groups = [];
|
||||
|
||||
for (const event of sorted) {
|
||||
if (used.has(event.id)) continue;
|
||||
|
||||
const group = [event];
|
||||
used.add(event.id);
|
||||
|
||||
let expanded = true;
|
||||
while (expanded) {
|
||||
expanded = false;
|
||||
for (const candidate of sorted) {
|
||||
if (used.has(candidate.id)) continue;
|
||||
|
||||
const connects = group.some(member =>
|
||||
eventsWithinThreshold(member, candidate, thresholdMinutes)
|
||||
);
|
||||
|
||||
if (connects) {
|
||||
group.push(candidate);
|
||||
used.add(candidate.id);
|
||||
expanded = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
groups.push(group);
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
function calculateStackLevels(events) {
|
||||
const levels = new Map();
|
||||
const sorted = [...events].sort((a, b) => a.start.getTime() - b.start.getTime());
|
||||
|
||||
for (const event of sorted) {
|
||||
let maxOverlappingLevel = -1;
|
||||
|
||||
for (const [id, level] of levels) {
|
||||
const other = events.find(e => e.id === id);
|
||||
if (other && eventsOverlap(event, other)) {
|
||||
maxOverlappingLevel = Math.max(maxOverlappingLevel, level);
|
||||
}
|
||||
}
|
||||
|
||||
levels.set(event.id, maxOverlappingLevel + 1);
|
||||
}
|
||||
|
||||
return levels;
|
||||
}
|
||||
|
||||
function allocateColumns(events) {
|
||||
const sorted = [...events].sort((a, b) => a.start.getTime() - b.start.getTime());
|
||||
const columns = [];
|
||||
|
||||
for (const event of sorted) {
|
||||
let placed = false;
|
||||
for (const column of columns) {
|
||||
const canFit = !column.some(e => eventsOverlap(event, e));
|
||||
if (canFit) {
|
||||
column.push(event);
|
||||
placed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!placed) {
|
||||
columns.push([event]);
|
||||
}
|
||||
}
|
||||
|
||||
return columns;
|
||||
}
|
||||
|
||||
function calculateEventPosition(start, end, config) {
|
||||
const startMinutes = start.getHours() * 60 + start.getMinutes();
|
||||
const endMinutes = end.getHours() * 60 + end.getMinutes();
|
||||
|
||||
const dayStartMinutes = config.dayStartHour * 60;
|
||||
const minuteHeight = config.hourHeight / 60;
|
||||
|
||||
const top = (startMinutes - dayStartMinutes) * minuteHeight;
|
||||
const height = (endMinutes - startMinutes) * minuteHeight;
|
||||
|
||||
return { top, height };
|
||||
}
|
||||
|
||||
function calculateColumnLayout(events, config) {
|
||||
const thresholdMinutes = config.gridStartThresholdMinutes ?? 30;
|
||||
|
||||
const result = {
|
||||
grids: [],
|
||||
stacked: []
|
||||
};
|
||||
|
||||
if (events.length === 0) return result;
|
||||
|
||||
const overlapGroups = findOverlapGroups(events);
|
||||
|
||||
for (const overlapGroup of overlapGroups) {
|
||||
if (overlapGroup.length === 1) {
|
||||
result.stacked.push({
|
||||
event: overlapGroup[0],
|
||||
stackLevel: 0
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const gridSubgroups = findGridCandidates(overlapGroup, thresholdMinutes);
|
||||
|
||||
const largestGridCandidate = gridSubgroups.reduce((max, g) =>
|
||||
g.length > max.length ? g : max, gridSubgroups[0]);
|
||||
|
||||
if (largestGridCandidate.length === overlapGroup.length) {
|
||||
const columns = allocateColumns(overlapGroup);
|
||||
const earliest = overlapGroup.reduce((min, e) =>
|
||||
e.start < min.start ? e : min, overlapGroup[0]);
|
||||
const position = calculateEventPosition(earliest.start, earliest.end, config);
|
||||
|
||||
result.grids.push({
|
||||
events: overlapGroup,
|
||||
columns,
|
||||
stackLevel: 0,
|
||||
position: { top: position.top }
|
||||
});
|
||||
} else {
|
||||
const levels = calculateStackLevels(overlapGroup);
|
||||
for (const event of overlapGroup) {
|
||||
result.stacked.push({
|
||||
event,
|
||||
stackLevel: levels.get(event.id) ?? 0
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Scenario Renderer
|
||||
// ============================================
|
||||
|
||||
const gridConfig = {
|
||||
hourHeight: 80,
|
||||
dayStartHour: 8,
|
||||
dayEndHour: 20,
|
||||
snapInterval: 15,
|
||||
gridStartThresholdMinutes: 30
|
||||
};
|
||||
|
||||
function formatTime(date) {
|
||||
return date.toLocaleTimeString('da-DK', { hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
|
||||
function createEventElement(event, config) {
|
||||
const element = document.createElement('swp-event');
|
||||
element.dataset.eventId = event.id;
|
||||
|
||||
const position = calculateEventPosition(event.start, event.end, config);
|
||||
element.style.position = 'absolute';
|
||||
element.style.top = `${position.top}px`;
|
||||
element.style.height = `${position.height}px`;
|
||||
element.style.left = '2px';
|
||||
element.style.right = '2px';
|
||||
|
||||
element.innerHTML = `
|
||||
<swp-event-time>${formatTime(event.start)} - ${formatTime(event.end)}</swp-event-time>
|
||||
<swp-event-title>${event.title}</swp-event-title>
|
||||
`;
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
function renderGridGroup(layout, config) {
|
||||
const group = document.createElement('swp-event-group');
|
||||
group.classList.add(`cols-${layout.columns.length}`);
|
||||
group.style.top = `${layout.position.top}px`;
|
||||
|
||||
// Stack level styling
|
||||
group.dataset.stackLink = JSON.stringify({ stackLevel: layout.stackLevel });
|
||||
if (layout.stackLevel > 0) {
|
||||
group.style.marginLeft = `${layout.stackLevel * 15}px`;
|
||||
group.style.zIndex = `${100 + layout.stackLevel}`;
|
||||
}
|
||||
|
||||
// Calculate height
|
||||
let maxBottom = 0;
|
||||
for (const event of layout.events) {
|
||||
const pos = calculateEventPosition(event.start, event.end, config);
|
||||
const eventBottom = pos.top + pos.height;
|
||||
if (eventBottom > maxBottom) maxBottom = eventBottom;
|
||||
}
|
||||
group.style.height = `${maxBottom - layout.position.top}px`;
|
||||
|
||||
// Create columns
|
||||
layout.columns.forEach(columnEvents => {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.style.position = 'relative';
|
||||
|
||||
columnEvents.forEach(event => {
|
||||
const eventEl = createEventElement(event, config);
|
||||
const pos = calculateEventPosition(event.start, event.end, config);
|
||||
eventEl.style.top = `${pos.top - layout.position.top}px`;
|
||||
eventEl.style.left = '0';
|
||||
eventEl.style.right = '0';
|
||||
wrapper.appendChild(eventEl);
|
||||
});
|
||||
|
||||
group.appendChild(wrapper);
|
||||
});
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
function renderStackedEvent(event, stackLevel, config) {
|
||||
const element = createEventElement(event, config);
|
||||
|
||||
element.dataset.stackLink = JSON.stringify({ stackLevel });
|
||||
|
||||
if (stackLevel > 0) {
|
||||
element.style.marginLeft = `${stackLevel * 15}px`;
|
||||
element.style.zIndex = `${100 + stackLevel}`;
|
||||
} else {
|
||||
element.style.zIndex = '100';
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
export function renderScenario(container, events, config = gridConfig) {
|
||||
container.innerHTML = '';
|
||||
|
||||
const layout = calculateColumnLayout(events, config);
|
||||
|
||||
// Render grids
|
||||
layout.grids.forEach(grid => {
|
||||
const groupEl = renderGridGroup(grid, config);
|
||||
container.appendChild(groupEl);
|
||||
});
|
||||
|
||||
// Render stacked events
|
||||
layout.stacked.forEach(item => {
|
||||
const eventEl = renderStackedEvent(item.event, item.stackLevel, config);
|
||||
container.appendChild(eventEl);
|
||||
});
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
export { calculateColumnLayout, gridConfig };
|
||||
307
.workbench/scenarios/v2-scenarios.html
Normal file
307
.workbench/scenarios/v2-scenarios.html
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="da">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>V2 Event Layout Engine - All Scenarios</title>
|
||||
<link rel="stylesheet" href="scenario-styles.css">
|
||||
<style>
|
||||
.scenarios-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.scenario-card {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.scenario-card h3 {
|
||||
margin-top: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.calendar-column {
|
||||
height: 600px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.summary {
|
||||
background: #f0f0f0;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.summary h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.pass-count { color: #4caf50; font-weight: bold; }
|
||||
.fail-count { color: #f44336; font-weight: bold; }
|
||||
|
||||
.expected-info {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
background: #f8f9fa;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="scenario-container" style="max-width: 1600px;">
|
||||
<h1>V2 Event Layout Engine - Visual Tests</h1>
|
||||
|
||||
<div class="summary">
|
||||
<h2>Test Summary</h2>
|
||||
<p>
|
||||
<span class="pass-count" id="pass-count">0</span> passed,
|
||||
<span class="fail-count" id="fail-count">0</span> failed
|
||||
</p>
|
||||
<p>Using V2 EventLayoutEngine with gridStartThresholdMinutes: 30</p>
|
||||
</div>
|
||||
|
||||
<div class="scenarios-grid" id="scenarios-container">
|
||||
<!-- Scenarios will be rendered here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { renderScenario, calculateColumnLayout, gridConfig } from './v2-scenario-renderer.js';
|
||||
import { ScenarioTestRunner } from './scenario-test-runner.js';
|
||||
|
||||
// ============================================
|
||||
// Scenario Definitions
|
||||
// ============================================
|
||||
|
||||
const scenarios = [
|
||||
{
|
||||
id: 'scenario-1',
|
||||
title: 'Scenario 1: No Overlap',
|
||||
description: 'Three sequential events with no time overlap',
|
||||
events: [
|
||||
{ id: 'S1A', title: 'Event A', start: new Date('2025-10-06T10:00'), end: new Date('2025-10-06T11:00') },
|
||||
{ id: 'S1B', title: 'Event B', start: new Date('2025-10-06T11:00'), end: new Date('2025-10-06T12:00') },
|
||||
{ id: 'S1C', title: 'Event C', start: new Date('2025-10-06T12:00'), end: new Date('2025-10-06T13:00') }
|
||||
],
|
||||
expected: [
|
||||
{ eventId: 'S1A', stackLevel: 0, type: 'stacked' },
|
||||
{ eventId: 'S1B', stackLevel: 0, type: 'stacked' },
|
||||
{ eventId: 'S1C', stackLevel: 0, type: 'stacked' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'scenario-2',
|
||||
title: 'Scenario 2: Column Sharing (Grid)',
|
||||
description: 'Two events starting at exactly the same time',
|
||||
events: [
|
||||
{ id: 'S2A', title: 'Event A', start: new Date('2025-10-06T10:00'), end: new Date('2025-10-06T11:00') },
|
||||
{ id: 'S2B', title: 'Event B', start: new Date('2025-10-06T10:00'), end: new Date('2025-10-06T11:00') }
|
||||
],
|
||||
expected: [
|
||||
{ eventId: 'S2A', stackLevel: 0, cols: 2, type: 'grid' },
|
||||
{ eventId: 'S2B', stackLevel: 0, cols: 2, type: 'grid' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'scenario-3',
|
||||
title: 'Scenario 3: Nested Stacking',
|
||||
description: 'Progressive nesting: A contains B, B contains C, C overlaps D',
|
||||
events: [
|
||||
{ id: 'S3A', title: 'Event A', start: new Date('2025-10-06T09:00'), end: new Date('2025-10-06T15:00') },
|
||||
{ id: 'S3B', title: 'Event B', start: new Date('2025-10-06T10:00'), end: new Date('2025-10-06T13:00') },
|
||||
{ id: 'S3C', title: 'Event C', start: new Date('2025-10-06T11:00'), end: new Date('2025-10-06T12:00') },
|
||||
{ id: 'S3D', title: 'Event D', start: new Date('2025-10-06T12:30'), end: new Date('2025-10-06T13:30') }
|
||||
],
|
||||
expected: [
|
||||
{ eventId: 'S3A', stackLevel: 0, type: 'stacked' },
|
||||
{ eventId: 'S3B', stackLevel: 1, type: 'stacked' },
|
||||
{ eventId: 'S3C', stackLevel: 2, type: 'stacked' },
|
||||
{ eventId: 'S3D', stackLevel: 2, type: 'stacked' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'scenario-4',
|
||||
title: 'Scenario 4: Complex Stacking',
|
||||
description: 'Long event A with nested B, C, D at different times',
|
||||
events: [
|
||||
{ id: 'S4A', title: 'Event A', start: new Date('2025-10-06T14:00'), end: new Date('2025-10-06T20:00') },
|
||||
{ id: 'S4B', title: 'Event B', start: new Date('2025-10-06T15:00'), end: new Date('2025-10-06T17:00') },
|
||||
{ id: 'S4C', title: 'Event C', start: new Date('2025-10-06T15:30'), end: new Date('2025-10-06T16:30') },
|
||||
{ id: 'S4D', title: 'Event D', start: new Date('2025-10-06T18:00'), end: new Date('2025-10-06T19:00') }
|
||||
],
|
||||
expected: [
|
||||
{ eventId: 'S4A', stackLevel: 0, type: 'stacked' },
|
||||
{ eventId: 'S4B', stackLevel: 1, type: 'stacked' },
|
||||
{ eventId: 'S4C', stackLevel: 2, type: 'stacked' },
|
||||
{ eventId: 'S4D', stackLevel: 1, type: 'stacked' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'scenario-5',
|
||||
title: 'Scenario 5: Three Column Share',
|
||||
description: 'Three events all starting at the same time',
|
||||
events: [
|
||||
{ id: 'S5A', title: 'Event A', start: new Date('2025-10-06T10:00'), end: new Date('2025-10-06T11:00') },
|
||||
{ id: 'S5B', title: 'Event B', start: new Date('2025-10-06T10:00'), end: new Date('2025-10-06T11:00') },
|
||||
{ id: 'S5C', title: 'Event C', start: new Date('2025-10-06T10:00'), end: new Date('2025-10-06T11:00') }
|
||||
],
|
||||
expected: [
|
||||
{ eventId: 'S5A', stackLevel: 0, cols: 3, type: 'grid' },
|
||||
{ eventId: 'S5B', stackLevel: 0, cols: 3, type: 'grid' },
|
||||
{ eventId: 'S5C', stackLevel: 0, cols: 3, type: 'grid' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'scenario-6',
|
||||
title: 'Scenario 6: Overlapping Pairs',
|
||||
description: 'Two independent pairs of overlapping events',
|
||||
events: [
|
||||
{ id: 'S6A', title: 'Event A', start: new Date('2025-10-06T10:00'), end: new Date('2025-10-06T12:00') },
|
||||
{ id: 'S6B', title: 'Event B', start: new Date('2025-10-06T11:00'), end: new Date('2025-10-06T12:00') },
|
||||
{ id: 'S6C', title: 'Event C', start: new Date('2025-10-06T13:00'), end: new Date('2025-10-06T15:00') },
|
||||
{ id: 'S6D', title: 'Event D', start: new Date('2025-10-06T14:00'), end: new Date('2025-10-06T15:00') }
|
||||
],
|
||||
expected: [
|
||||
{ eventId: 'S6A', stackLevel: 0, type: 'stacked' },
|
||||
{ eventId: 'S6B', stackLevel: 1, type: 'stacked' },
|
||||
{ eventId: 'S6C', stackLevel: 0, type: 'stacked' },
|
||||
{ eventId: 'S6D', stackLevel: 1, type: 'stacked' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'scenario-7',
|
||||
title: 'Scenario 7: Long Event Container',
|
||||
description: 'Long event containing two non-overlapping events',
|
||||
events: [
|
||||
{ id: 'S7A', title: 'Event A', start: new Date('2025-10-06T09:00'), end: new Date('2025-10-06T15:00') },
|
||||
{ id: 'S7B', title: 'Event B', start: new Date('2025-10-06T10:00'), end: new Date('2025-10-06T11:00') },
|
||||
{ id: 'S7C', title: 'Event C', start: new Date('2025-10-06T12:00'), end: new Date('2025-10-06T13:00') }
|
||||
],
|
||||
expected: [
|
||||
{ eventId: 'S7A', stackLevel: 0, type: 'stacked' },
|
||||
{ eventId: 'S7B', stackLevel: 1, type: 'stacked' },
|
||||
{ eventId: 'S7C', stackLevel: 1, type: 'stacked' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'scenario-8',
|
||||
title: 'Scenario 8: Edge-Adjacent Events',
|
||||
description: 'Events that touch but do not overlap (end === start)',
|
||||
events: [
|
||||
{ id: 'S8A', title: 'Event A', start: new Date('2025-10-06T10:00'), end: new Date('2025-10-06T11:00') },
|
||||
{ id: 'S8B', title: 'Event B', start: new Date('2025-10-06T11:00'), end: new Date('2025-10-06T12:00') }
|
||||
],
|
||||
expected: [
|
||||
{ eventId: 'S8A', stackLevel: 0, type: 'stacked' },
|
||||
{ eventId: 'S8B', stackLevel: 0, type: 'stacked' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'scenario-9',
|
||||
title: 'Scenario 9: End-to-Start Chain',
|
||||
description: 'Events linked by conflict chain: A->B->C',
|
||||
events: [
|
||||
{ id: 'S9A', title: 'Event A', start: new Date('2025-10-06T12:00'), end: new Date('2025-10-06T13:00') },
|
||||
{ id: 'S9B', title: 'Event B', start: new Date('2025-10-06T12:30'), end: new Date('2025-10-06T13:30') },
|
||||
{ id: 'S9C', title: 'Event C', start: new Date('2025-10-06T13:15'), end: new Date('2025-10-06T15:00') }
|
||||
],
|
||||
expected: [
|
||||
{ eventId: 'S9A', stackLevel: 0, cols: 2, type: 'grid' },
|
||||
{ eventId: 'S9B', stackLevel: 0, cols: 2, type: 'grid' },
|
||||
{ eventId: 'S9C', stackLevel: 0, cols: 2, type: 'grid' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'scenario-10',
|
||||
title: 'Scenario 10: Four Column Grid',
|
||||
description: 'Four events all starting at the same time',
|
||||
events: [
|
||||
{ id: 'S10A', title: 'Event A', start: new Date('2025-10-06T14:00'), end: new Date('2025-10-06T15:00') },
|
||||
{ id: 'S10B', title: 'Event B', start: new Date('2025-10-06T14:00'), end: new Date('2025-10-06T15:00') },
|
||||
{ id: 'S10C', title: 'Event C', start: new Date('2025-10-06T14:00'), end: new Date('2025-10-06T15:00') },
|
||||
{ id: 'S10D', title: 'Event D', start: new Date('2025-10-06T14:00'), end: new Date('2025-10-06T15:00') }
|
||||
],
|
||||
expected: [
|
||||
{ eventId: 'S10A', stackLevel: 0, cols: 4, type: 'grid' },
|
||||
{ eventId: 'S10B', stackLevel: 0, cols: 4, type: 'grid' },
|
||||
{ eventId: 'S10C', stackLevel: 0, cols: 4, type: 'grid' },
|
||||
{ eventId: 'S10D', stackLevel: 0, cols: 4, type: 'grid' }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// ============================================
|
||||
// Render All Scenarios
|
||||
// ============================================
|
||||
|
||||
const container = document.getElementById('scenarios-container');
|
||||
let passCount = 0;
|
||||
let failCount = 0;
|
||||
|
||||
scenarios.forEach(scenario => {
|
||||
// Create card
|
||||
const card = document.createElement('div');
|
||||
card.className = 'scenario-card';
|
||||
card.innerHTML = `
|
||||
<h3>
|
||||
${scenario.title}
|
||||
<span class="test-badge test-pending" id="badge-${scenario.id}">Testing...</span>
|
||||
</h3>
|
||||
<p>${scenario.description}</p>
|
||||
<div class="expected-info">
|
||||
Expected: ${scenario.expected.map(e =>
|
||||
e.type === 'grid'
|
||||
? `${e.eventId} (grid cols-${e.cols})`
|
||||
: `${e.eventId} (level ${e.stackLevel})`
|
||||
).join(', ')}
|
||||
</div>
|
||||
<div class="calendar-column" id="column-${scenario.id}"></div>
|
||||
<div id="results-${scenario.id}"></div>
|
||||
`;
|
||||
container.appendChild(card);
|
||||
|
||||
// Render events
|
||||
const columnEl = document.getElementById(`column-${scenario.id}`);
|
||||
renderScenario(columnEl, scenario.events);
|
||||
|
||||
// Run validation
|
||||
const results = ScenarioTestRunner.validateScenario(scenario.id, scenario.expected);
|
||||
|
||||
// Update badge
|
||||
const badge = document.getElementById(`badge-${scenario.id}`);
|
||||
badge.className = `test-badge ${results.passed ? 'test-passed' : 'test-failed'}`;
|
||||
badge.textContent = results.passed ? 'PASSED' : 'FAILED';
|
||||
|
||||
if (results.passed) {
|
||||
passCount++;
|
||||
} else {
|
||||
failCount++;
|
||||
}
|
||||
|
||||
// Show detailed results if failed
|
||||
if (!results.passed) {
|
||||
const resultsEl = document.getElementById(`results-${scenario.id}`);
|
||||
resultsEl.innerHTML = '<div class="test-details"><h4>Failed checks:</h4>' +
|
||||
results.results
|
||||
.filter(r => !r.passed)
|
||||
.map(r => `<div class="test-result-line failed">${r.eventId}: ${r.message}</div>`)
|
||||
.join('') +
|
||||
'</div>';
|
||||
}
|
||||
});
|
||||
|
||||
// Update summary
|
||||
document.getElementById('pass-count').textContent = passCount;
|
||||
document.getElementById('fail-count').textContent = failCount;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue