fix(timeline): stabilize overlay lifecycle
This commit is contained in:
@@ -438,20 +438,22 @@ async function openAllocationContextMenuAtOffset(
|
||||
locator: ReturnType<Page["locator"]>,
|
||||
xOffset: number,
|
||||
) {
|
||||
const box = await locator.boundingBox();
|
||||
expect(box).not.toBeNull();
|
||||
if (!box) {
|
||||
throw new Error("Expected allocation segment to be visible before context click");
|
||||
}
|
||||
const target = await resolveAllocationContextMenuTarget(locator);
|
||||
const box = await readBoundingBox(target);
|
||||
|
||||
await page.mouse.click(box.x + xOffset, box.y + box.height / 2, { button: "right" });
|
||||
await page.mouse.click(
|
||||
box.x + Math.min(Math.max(xOffset, 2), Math.max(box.width - 2, 2)),
|
||||
box.y + box.height / 2,
|
||||
{ button: "right" },
|
||||
);
|
||||
}
|
||||
|
||||
async function openContextMenuAtCenter(
|
||||
page: Page,
|
||||
locator: ReturnType<Page["locator"]>,
|
||||
) {
|
||||
const box = await readBoundingBox(locator);
|
||||
const target = await resolveAllocationContextMenuTarget(locator);
|
||||
const box = await readBoundingBox(target);
|
||||
await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2, { button: "right" });
|
||||
}
|
||||
|
||||
@@ -500,6 +502,11 @@ async function readBoundingBox(locator: ReturnType<Page["locator"]>) {
|
||||
return box;
|
||||
}
|
||||
|
||||
async function resolveAllocationContextMenuTarget(locator: ReturnType<Page["locator"]>) {
|
||||
const interactionTarget = locator.locator("[data-allocation-interaction='body']").first();
|
||||
return (await interactionTarget.count()) > 0 ? interactionTarget : locator;
|
||||
}
|
||||
|
||||
async function listRenderedAllocationSegments(
|
||||
row: ReturnType<Page["locator"]>,
|
||||
allocationId?: string,
|
||||
@@ -588,6 +595,16 @@ async function findVisibleAllocationSegmentForResize(
|
||||
selector: string,
|
||||
): Promise<VisibleAllocationSegment | null> {
|
||||
return page.locator(selector).evaluateAll((elements) => {
|
||||
const scrollContainer = document.querySelector<HTMLElement>(
|
||||
"div.app-surface.relative.z-0.flex-1.overflow-auto",
|
||||
);
|
||||
const stickyHeaderBottom = scrollContainer
|
||||
? Array.from(scrollContainer.querySelectorAll<HTMLElement>(".sticky.top-0")).reduce(
|
||||
(maxBottom, element) => Math.max(maxBottom, element.getBoundingClientRect().bottom),
|
||||
0,
|
||||
)
|
||||
: 0;
|
||||
const safeTop = stickyHeaderBottom > 0 ? stickyHeaderBottom + 8 : 48;
|
||||
const candidates: Array<{
|
||||
allocationId: string;
|
||||
segmentStart: string | null;
|
||||
@@ -608,7 +625,7 @@ async function findVisibleAllocationSegmentForResize(
|
||||
rect.height >= 10 &&
|
||||
rect.right > 48 &&
|
||||
rect.left < window.innerWidth - 48 &&
|
||||
rect.bottom > 0 &&
|
||||
rect.bottom > safeTop &&
|
||||
rect.top < window.innerHeight;
|
||||
|
||||
if (!isVisible) {
|
||||
@@ -625,7 +642,8 @@ async function findVisibleAllocationSegmentForResize(
|
||||
rect.width >= 36 &&
|
||||
rect.width <= 220 &&
|
||||
rect.left >= 48 &&
|
||||
rect.right <= window.innerWidth - 48;
|
||||
rect.right <= window.innerWidth - 48 &&
|
||||
rect.top >= safeTop;
|
||||
|
||||
if (!isPreferred) {
|
||||
continue;
|
||||
@@ -1197,6 +1215,26 @@ test.describe("Timeline", () => {
|
||||
await expect(cancelButton).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("timeline allocation popovers close cleanly across view switches", async ({ page }) => {
|
||||
const allocation = page
|
||||
.locator("[data-timeline-entry-type='allocation'][data-allocation-id]")
|
||||
.first();
|
||||
await expect(allocation).toBeVisible();
|
||||
await openContextMenuAtCenter(page, allocation);
|
||||
|
||||
const allocationPopover = page.getByTestId("timeline-allocation-popover");
|
||||
await expect(page.getByTestId("timeline-allocation-popover-loading")).toHaveCount(0, {
|
||||
timeout: 2_000,
|
||||
});
|
||||
await expect(allocationPopover).toBeVisible();
|
||||
|
||||
await switchToProjectView(page);
|
||||
await expect(allocationPopover).toHaveCount(0);
|
||||
await expect(page.getByTestId("timeline-allocation-popover-loading")).toHaveCount(0);
|
||||
await expect(page.getByTestId("timeline-allocation-popover-error")).toHaveCount(0);
|
||||
await expect(page.getByTestId("timeline-allocation-popover-unavailable")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("project bars stay attached to the pointer during fast drag", async ({ page }) => {
|
||||
await switchToProjectView(page);
|
||||
await expect(page.locator("[data-timeline-entry-type='project-bar']").first()).toBeVisible();
|
||||
@@ -1351,7 +1389,7 @@ test.describe("Timeline", () => {
|
||||
expect(rightResizeAssignments).toHaveLength(1);
|
||||
expect(rightResizeAssignments[0]?.id).toBe(scenario.assignmentId);
|
||||
expect(rightResizeAssignments[0]?.startDate).toBe("2026-04-06");
|
||||
expect(rightResizeAssignments[0]?.endDate > "2026-04-17").toBe(true);
|
||||
expect(rightResizeAssignments[0]!.endDate > "2026-04-17").toBe(true);
|
||||
|
||||
const resizeStart = await measureAllocationResizeStartGap(page, projectAllocationSelector);
|
||||
expect(resizeStart.widthGain).toBeGreaterThan(48);
|
||||
@@ -1374,7 +1412,7 @@ test.describe("Timeline", () => {
|
||||
.not.toBe("2026-04-06");
|
||||
expect(leftResizeAssignments).toHaveLength(1);
|
||||
expect(leftResizeAssignments[0]?.id).toBe(scenario.assignmentId);
|
||||
expect(leftResizeAssignments[0]?.startDate < "2026-04-06").toBe(true);
|
||||
expect(leftResizeAssignments[0]!.startDate < "2026-04-06").toBe(true);
|
||||
expect(leftResizeAssignments[0]?.endDate).toBe(rightResizeAssignments[0]?.endDate);
|
||||
} finally {
|
||||
cleanupTimelineSegmentScenario(scenario.projectId, scenario.resourceId);
|
||||
@@ -1429,7 +1467,7 @@ test.describe("Timeline", () => {
|
||||
expect(rightResizeDemands).toHaveLength(1);
|
||||
expect(rightResizeDemands[0]?.id).toBe(scenario.demandId);
|
||||
expect(rightResizeDemands[0]?.startDate).toBe("2026-04-07");
|
||||
expect(rightResizeDemands[0]?.endDate > "2026-04-16").toBe(true);
|
||||
expect(rightResizeDemands[0]!.endDate > "2026-04-16").toBe(true);
|
||||
expect(rightResizeDemands[0]?.headcount).toBe(2);
|
||||
expect(rightResizeDemands[0]?.status).toBe("PROPOSED");
|
||||
|
||||
@@ -1461,7 +1499,7 @@ test.describe("Timeline", () => {
|
||||
.not.toBe("2026-04-07");
|
||||
expect(leftResizeDemands).toHaveLength(1);
|
||||
expect(leftResizeDemands[0]?.id).toBe(scenario.demandId);
|
||||
expect(leftResizeDemands[0]?.startDate < "2026-04-07").toBe(true);
|
||||
expect(leftResizeDemands[0]!.startDate < "2026-04-07").toBe(true);
|
||||
expect(leftResizeDemands[0]?.endDate).toBe(rightResizeDemands[0]?.endDate);
|
||||
expect(leftResizeDemands[0]?.headcount).toBe(2);
|
||||
expect(leftResizeDemands[0]?.status).toBe("PROPOSED");
|
||||
|
||||
Reference in New Issue
Block a user