fix(timeline): harden project view interactions
This commit is contained in:
+446
-33
@@ -109,6 +109,14 @@ type TimelineSegmentScenario = {
|
||||
resourceEid: string;
|
||||
};
|
||||
|
||||
type TimelineDemandScenario = {
|
||||
demandId: string;
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
projectShortCode: string;
|
||||
resourceId: string;
|
||||
};
|
||||
|
||||
function createTimelineSegmentScenario(suffix: string): TimelineSegmentScenario {
|
||||
return runDbJson<TimelineSegmentScenario>(`
|
||||
const availability = {
|
||||
@@ -184,6 +192,96 @@ function createTimelineSegmentScenario(suffix: string): TimelineSegmentScenario
|
||||
`);
|
||||
}
|
||||
|
||||
function createTimelineDemandScenario(suffix: string): TimelineDemandScenario {
|
||||
return runDbJson<TimelineDemandScenario>(`
|
||||
const availability = {
|
||||
monday: 8,
|
||||
tuesday: 8,
|
||||
wednesday: 8,
|
||||
thursday: 8,
|
||||
friday: 8,
|
||||
saturday: 0,
|
||||
sunday: 0,
|
||||
};
|
||||
|
||||
const resource = await prisma.resource.create({
|
||||
data: {
|
||||
eid: ${JSON.stringify(`e2e.timeline.demand.${suffix}`)},
|
||||
displayName: ${JSON.stringify(`E2E Timeline Demand Resource ${suffix}`)},
|
||||
email: ${JSON.stringify(`e2e.timeline.demand.${suffix}@capakraken.dev`)},
|
||||
chapter: "E2E",
|
||||
lcrCents: 5000,
|
||||
ucrCents: 9000,
|
||||
availability,
|
||||
skills: [],
|
||||
dynamicFields: {},
|
||||
resourceType: "EMPLOYEE",
|
||||
chgResponsibility: true,
|
||||
rolledOff: false,
|
||||
departed: false,
|
||||
fte: 1,
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
const project = await prisma.project.create({
|
||||
data: {
|
||||
shortCode: ${JSON.stringify(`E2EDM${suffix.slice(-6).toUpperCase()}`)},
|
||||
name: ${JSON.stringify(`E2E Timeline Demand ${suffix}`)},
|
||||
orderType: "CHARGEABLE",
|
||||
allocationType: "EXT",
|
||||
budgetCents: 1500000,
|
||||
startDate: new Date("2026-04-01T00:00:00.000Z"),
|
||||
endDate: new Date("2026-04-30T00:00:00.000Z"),
|
||||
status: "ACTIVE",
|
||||
staffingReqs: [],
|
||||
dynamicFields: {},
|
||||
},
|
||||
select: { id: true, name: true, shortCode: true },
|
||||
});
|
||||
|
||||
const demand = await prisma.demandRequirement.create({
|
||||
data: {
|
||||
projectId: project.id,
|
||||
startDate: new Date("2026-04-07T00:00:00.000Z"),
|
||||
endDate: new Date("2026-04-16T00:00:00.000Z"),
|
||||
hoursPerDay: 6,
|
||||
percentage: 100,
|
||||
role: "E2E Open Demand",
|
||||
headcount: 2,
|
||||
budgetCents: 240000,
|
||||
status: "PROPOSED",
|
||||
metadata: {},
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
await prisma.assignment.create({
|
||||
data: {
|
||||
resourceId: resource.id,
|
||||
projectId: project.id,
|
||||
startDate: new Date("2026-04-08T00:00:00.000Z"),
|
||||
endDate: new Date("2026-04-10T00:00:00.000Z"),
|
||||
hoursPerDay: 4,
|
||||
percentage: 50,
|
||||
role: "E2E Seeded Assignment",
|
||||
dailyCostCents: 20000,
|
||||
status: "ACTIVE",
|
||||
metadata: {},
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
console.log(JSON.stringify({
|
||||
demandId: demand.id,
|
||||
projectId: project.id,
|
||||
projectName: project.name,
|
||||
projectShortCode: project.shortCode,
|
||||
resourceId: resource.id,
|
||||
}));
|
||||
`);
|
||||
}
|
||||
|
||||
function cleanupTimelineSegmentScenario(projectId: string, resourceId: string) {
|
||||
runDbJson<null>(`
|
||||
await prisma.assignment.deleteMany({
|
||||
@@ -202,6 +300,28 @@ function cleanupTimelineSegmentScenario(projectId: string, resourceId: string) {
|
||||
`);
|
||||
}
|
||||
|
||||
function cleanupTimelineDemandScenario(projectId: string, resourceId: string) {
|
||||
runDbJson<null>(`
|
||||
await prisma.assignment.deleteMany({
|
||||
where: { projectId: ${JSON.stringify(projectId)} },
|
||||
});
|
||||
|
||||
await prisma.demandRequirement.deleteMany({
|
||||
where: { projectId: ${JSON.stringify(projectId)} },
|
||||
});
|
||||
|
||||
await prisma.project.deleteMany({
|
||||
where: { id: ${JSON.stringify(projectId)} },
|
||||
});
|
||||
|
||||
await prisma.resource.deleteMany({
|
||||
where: { id: ${JSON.stringify(resourceId)} },
|
||||
});
|
||||
|
||||
console.log("null");
|
||||
`);
|
||||
}
|
||||
|
||||
function listScenarioAssignments(projectId: string) {
|
||||
return runDbJson<Array<{ id: string; startDate: string; endDate: string }>>(`
|
||||
const assignments = await prisma.assignment.findMany({
|
||||
@@ -220,6 +340,26 @@ function listScenarioAssignments(projectId: string) {
|
||||
`);
|
||||
}
|
||||
|
||||
function listScenarioDemands(projectId: string) {
|
||||
return runDbJson<Array<{ id: string; startDate: string; endDate: string; headcount: number; status: string }>>(`
|
||||
const demands = await prisma.demandRequirement.findMany({
|
||||
where: { projectId: ${JSON.stringify(projectId)} },
|
||||
orderBy: [{ startDate: "asc" }, { endDate: "asc" }],
|
||||
select: { id: true, startDate: true, endDate: true, headcount: true, status: true },
|
||||
});
|
||||
|
||||
console.log(JSON.stringify(
|
||||
demands.map((entry) => ({
|
||||
id: entry.id,
|
||||
startDate: entry.startDate.toISOString().slice(0, 10),
|
||||
endDate: entry.endDate.toISOString().slice(0, 10),
|
||||
headcount: entry.headcount,
|
||||
status: entry.status,
|
||||
})),
|
||||
));
|
||||
`);
|
||||
}
|
||||
|
||||
function readScenarioSnapshot(projectId: string, resourceId: string, resourceEid: string) {
|
||||
return runDbJson<{
|
||||
resource: { id: string; eid: string; displayName: string } | null;
|
||||
@@ -665,12 +805,65 @@ async function measureAllocationResizeStartGap(page: Page, locatorString: string
|
||||
};
|
||||
}
|
||||
|
||||
async function switchToProjectView(page: Page, readySelector?: string) {
|
||||
await page.getByRole("button", { name: "Project view" }).click();
|
||||
if (readySelector) {
|
||||
await expect(page.locator(readySelector).first()).toBeVisible();
|
||||
} else {
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const projectRows = await page.getByTestId("timeline-project-resource-row-canvas").count();
|
||||
const projectBars = await page.locator("[data-timeline-entry-type='project-bar']").count();
|
||||
const demandBars = await page.locator("[data-timeline-entry-type='demand']").count();
|
||||
const emptyStates = await page.getByText(/No projects in this time range/).count();
|
||||
return projectRows + projectBars + demandBars + emptyStates;
|
||||
}, { timeout: 10_000 })
|
||||
.not.toBe(0);
|
||||
}
|
||||
await expect(page.getByTestId("timeline-resource-row-canvas")).toHaveCount(0);
|
||||
}
|
||||
|
||||
async function switchToResourceView(page: Page, readySelector?: string) {
|
||||
await page.getByRole("button", { name: "Resource view" }).click();
|
||||
if (readySelector) {
|
||||
await expect(page.locator(readySelector).first()).toBeVisible();
|
||||
} else {
|
||||
await expect(page.getByTestId("timeline-resource-row-canvas").first()).toBeVisible();
|
||||
}
|
||||
await expect(page.getByTestId("timeline-project-resource-row-canvas")).toHaveCount(0);
|
||||
}
|
||||
|
||||
async function ensureOpenDemandVisibilityEnabled(page: Page) {
|
||||
await page.evaluate(() => {
|
||||
const raw = window.localStorage.getItem("capakraken_prefs");
|
||||
const parsed = raw ? (JSON.parse(raw) as Record<string, unknown>) : {};
|
||||
window.localStorage.setItem(
|
||||
"capakraken_prefs",
|
||||
JSON.stringify({
|
||||
...parsed,
|
||||
showDemandProjects: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
await page.reload({ waitUntil: "domcontentloaded" });
|
||||
}
|
||||
|
||||
test.describe("Timeline", () => {
|
||||
test.describe.configure({ mode: "serial" });
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
localStorage.setItem("capakraken_theme", JSON.stringify({ mode: "dark" }));
|
||||
localStorage.setItem(
|
||||
"capakraken_prefs",
|
||||
JSON.stringify({
|
||||
hideCompletedProjects: true,
|
||||
timelineDisplayMode: "strip",
|
||||
heatmapColorScheme: "green-red",
|
||||
showDemandProjects: true,
|
||||
blinkOverbookedDays: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
await signInAsAdmin(page);
|
||||
await page.goto("/timeline");
|
||||
@@ -687,14 +880,49 @@ test.describe("Timeline", () => {
|
||||
});
|
||||
|
||||
test("can switch between resource and project view", async ({ page }) => {
|
||||
await page.click("text=Project view");
|
||||
await switchToProjectView(page);
|
||||
await expect(
|
||||
page.locator("text=0 projects").or(page.locator("text=/\\d+ projects/")),
|
||||
).toBeVisible();
|
||||
await page.click("text=Resource view");
|
||||
await switchToResourceView(page);
|
||||
await expect(page.locator("text=/\\d+ resources/")).toBeVisible();
|
||||
});
|
||||
|
||||
test("view toggle stays disabled until the initial timeline load becomes interactive", async ({ page }) => {
|
||||
const suffix = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
||||
const scenario = createTimelineSegmentScenario(suffix);
|
||||
|
||||
try {
|
||||
await page.goto(
|
||||
`/timeline?startDate=2026-04-01&days=31&eids=${scenario.resourceEid}`,
|
||||
{ waitUntil: "domcontentloaded" },
|
||||
);
|
||||
|
||||
const projectButton = page.getByRole("button", { name: "Project view" });
|
||||
const resourceButton = page.getByRole("button", { name: "Resource view" });
|
||||
const resourceRowSelector =
|
||||
`[data-testid="timeline-resource-row-canvas"][data-resource-eid="${scenario.resourceEid}"]`;
|
||||
const projectRowSelector =
|
||||
`[data-testid="timeline-project-resource-row-canvas"][data-project-id="${scenario.projectId}"][data-resource-id="${scenario.resourceId}"]`;
|
||||
|
||||
await expect(projectButton).toBeDisabled();
|
||||
await expect(resourceButton).toBeDisabled();
|
||||
|
||||
await expect(page.locator(resourceRowSelector)).toBeVisible();
|
||||
await expect(projectButton).toBeEnabled();
|
||||
await expect(resourceButton).toBeEnabled();
|
||||
await expect(resourceButton).toHaveAttribute("aria-pressed", "true");
|
||||
|
||||
await projectButton.click();
|
||||
|
||||
await expect(page.locator(projectRowSelector)).toBeVisible();
|
||||
await expect(projectButton).toHaveAttribute("aria-pressed", "true");
|
||||
await expect(resourceButton).toHaveAttribute("aria-pressed", "false");
|
||||
} finally {
|
||||
cleanupTimelineSegmentScenario(scenario.projectId, scenario.resourceId);
|
||||
}
|
||||
});
|
||||
|
||||
test("can navigate forward and back", async ({ page }) => {
|
||||
const todayBtn = page.locator("button", { hasText: "Today" });
|
||||
await expect(todayBtn).toBeVisible();
|
||||
@@ -786,9 +1014,7 @@ test.describe("Timeline", () => {
|
||||
await expect(allocationPopoverField).toBeVisible();
|
||||
await page.mouse.click(40, 40);
|
||||
|
||||
await page.getByText("Project view").click();
|
||||
await expect(page.getByText(/projects/)).toBeVisible();
|
||||
await page.waitForTimeout(500);
|
||||
await switchToProjectView(page);
|
||||
|
||||
const projectHoverTarget = page.getByTestId("timeline-project-resource-row-canvas").first();
|
||||
const projectHoverBox = await projectHoverTarget.boundingBox();
|
||||
@@ -837,8 +1063,7 @@ test.describe("Timeline", () => {
|
||||
});
|
||||
|
||||
test("right clicking a project header strip opens the project panel", async ({ page }) => {
|
||||
await page.getByText("Project view").click();
|
||||
await expect(page.getByText(/projects/)).toBeVisible();
|
||||
await switchToProjectView(page);
|
||||
|
||||
const projectBar = page
|
||||
.locator("[data-timeline-entry-type='project-bar'][data-timeline-project-id]")
|
||||
@@ -973,8 +1198,7 @@ test.describe("Timeline", () => {
|
||||
});
|
||||
|
||||
test("project bars stay attached to the pointer during fast drag", async ({ page }) => {
|
||||
await page.getByText("Project view").click();
|
||||
await expect(page.getByText(/projects/)).toBeVisible();
|
||||
await switchToProjectView(page);
|
||||
await expect(page.locator("[data-timeline-entry-type='project-bar']").first()).toBeVisible();
|
||||
|
||||
const projectId = await findVisibleTimelineEntryId(
|
||||
@@ -1066,6 +1290,186 @@ test.describe("Timeline", () => {
|
||||
expect(result.leftEdgeGain).toBeGreaterThan(36);
|
||||
});
|
||||
|
||||
test("project view resource allocations resize with a live preview before mouseup", async ({
|
||||
page,
|
||||
}) => {
|
||||
test.setTimeout(60_000);
|
||||
const suffix = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
||||
const scenario = createTimelineSegmentScenario(suffix);
|
||||
|
||||
try {
|
||||
await page.goto(`/timeline?startDate=2026-04-01&days=30&eids=${scenario.resourceEid}`, {
|
||||
waitUntil: "domcontentloaded",
|
||||
});
|
||||
const resourceRowSelector =
|
||||
`[data-testid="timeline-resource-row-canvas"][data-resource-eid="${scenario.resourceEid}"]`;
|
||||
const projectRowSelector =
|
||||
`[data-testid="timeline-project-resource-row-canvas"][data-project-id="${scenario.projectId}"][data-resource-id="${scenario.resourceId}"]`;
|
||||
const projectAllocationSelector =
|
||||
`${projectRowSelector} [data-timeline-entry-type="allocation"][data-allocation-id="${scenario.assignmentId}"]`;
|
||||
|
||||
await expect(page.locator(resourceRowSelector)).toBeVisible();
|
||||
await expect(
|
||||
page.locator(
|
||||
`[data-timeline-entry-type="allocation"][data-allocation-id="${scenario.assignmentId}"]`,
|
||||
).first(),
|
||||
).toBeVisible();
|
||||
|
||||
await switchToProjectView(page, projectRowSelector);
|
||||
await expect(page.getByText(scenario.projectName).first()).toBeVisible();
|
||||
|
||||
const projectAllocation = page.locator(projectAllocationSelector).first();
|
||||
await expect(projectAllocation).toBeVisible();
|
||||
await expect
|
||||
.poll(async () =>
|
||||
projectAllocation.evaluate((element) => ({
|
||||
segmentStart: element.getAttribute("data-allocation-segment-start"),
|
||||
segmentEnd: element.getAttribute("data-allocation-segment-end"),
|
||||
})),
|
||||
)
|
||||
.toEqual({ segmentStart: null, segmentEnd: null });
|
||||
|
||||
const resizeEnd = await measureAllocationResizeGap(page, projectAllocationSelector);
|
||||
expect(resizeEnd.widthGain).toBeGreaterThan(64);
|
||||
expect(resizeEnd.rightEdgeGain).toBeGreaterThan(48);
|
||||
let rightResizeAssignments: Array<{ id: string; startDate: string; endDate: string }> = [];
|
||||
await expect
|
||||
.poll(() => {
|
||||
rightResizeAssignments = listScenarioAssignments(scenario.projectId);
|
||||
if (rightResizeAssignments.length !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [assignment] = rightResizeAssignments;
|
||||
if (!assignment || assignment.id !== scenario.assignmentId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return assignment.endDate;
|
||||
}, { timeout: 15_000 })
|
||||
.not.toBe("2026-04-17");
|
||||
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);
|
||||
|
||||
const resizeStart = await measureAllocationResizeStartGap(page, projectAllocationSelector);
|
||||
expect(resizeStart.widthGain).toBeGreaterThan(48);
|
||||
expect(resizeStart.leftEdgeGain).toBeGreaterThan(36);
|
||||
let leftResizeAssignments: Array<{ id: string; startDate: string; endDate: string }> = [];
|
||||
await expect
|
||||
.poll(() => {
|
||||
leftResizeAssignments = listScenarioAssignments(scenario.projectId);
|
||||
if (leftResizeAssignments.length !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [assignment] = leftResizeAssignments;
|
||||
if (!assignment || assignment.id !== scenario.assignmentId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return assignment.startDate;
|
||||
}, { timeout: 15_000 })
|
||||
.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]?.endDate).toBe(rightResizeAssignments[0]?.endDate);
|
||||
} finally {
|
||||
cleanupTimelineSegmentScenario(scenario.projectId, scenario.resourceId);
|
||||
}
|
||||
});
|
||||
|
||||
test("project view demand bars resize with a live preview before mouseup", async ({ page }) => {
|
||||
const suffix = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
||||
const scenario = createTimelineDemandScenario(suffix);
|
||||
|
||||
try {
|
||||
await page.goto(
|
||||
`/timeline?startDate=2026-04-01&days=31&projectIds=${scenario.projectId}`,
|
||||
{ waitUntil: "domcontentloaded" },
|
||||
);
|
||||
await ensureOpenDemandVisibilityEnabled(page);
|
||||
const demandRowSelector =
|
||||
`[data-project-demand-row="true"][data-project-id="${scenario.projectId}"]`;
|
||||
const demandSelector =
|
||||
`${demandRowSelector} [data-timeline-entry-type="demand"][data-allocation-id="${scenario.demandId}"]`;
|
||||
|
||||
await switchToProjectView(page, demandRowSelector);
|
||||
await expect(page.getByText(scenario.projectName).first()).toBeVisible();
|
||||
await expect(page.locator(demandSelector)).toBeVisible();
|
||||
|
||||
const resizeEnd = await measureAllocationResizeGap(page, demandSelector);
|
||||
expect(resizeEnd.widthGain).toBeGreaterThan(48);
|
||||
expect(resizeEnd.rightEdgeGain).toBeGreaterThan(36);
|
||||
|
||||
let rightResizeDemands: Array<{
|
||||
id: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
headcount: number;
|
||||
status: string;
|
||||
}> = [];
|
||||
await expect
|
||||
.poll(() => {
|
||||
rightResizeDemands = listScenarioDemands(scenario.projectId);
|
||||
if (rightResizeDemands.length !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [demand] = rightResizeDemands;
|
||||
if (!demand || demand.id !== scenario.demandId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return demand.endDate;
|
||||
}, { timeout: 15_000 })
|
||||
.not.toBe("2026-04-16");
|
||||
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]?.headcount).toBe(2);
|
||||
expect(rightResizeDemands[0]?.status).toBe("PROPOSED");
|
||||
|
||||
const resizeStart = await measureAllocationResizeStartGap(page, demandSelector);
|
||||
expect(resizeStart.widthGain).toBeGreaterThan(36);
|
||||
expect(resizeStart.leftEdgeGain).toBeGreaterThan(24);
|
||||
|
||||
let leftResizeDemands: Array<{
|
||||
id: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
headcount: number;
|
||||
status: string;
|
||||
}> = [];
|
||||
await expect
|
||||
.poll(() => {
|
||||
leftResizeDemands = listScenarioDemands(scenario.projectId);
|
||||
if (leftResizeDemands.length !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [demand] = leftResizeDemands;
|
||||
if (!demand || demand.id !== scenario.demandId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return demand.startDate;
|
||||
}, { timeout: 15_000 })
|
||||
.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]?.endDate).toBe(rightResizeDemands[0]?.endDate);
|
||||
expect(leftResizeDemands[0]?.headcount).toBe(2);
|
||||
expect(leftResizeDemands[0]?.status).toBe("PROPOSED");
|
||||
} finally {
|
||||
cleanupTimelineDemandScenario(scenario.projectId, scenario.resourceId);
|
||||
}
|
||||
});
|
||||
|
||||
test("resource timeline supports resizing, moving, carving, and recreating segmented allocations with persisted dates", async ({
|
||||
page,
|
||||
}) => {
|
||||
@@ -1537,8 +1941,9 @@ test.describe("Timeline", () => {
|
||||
).first();
|
||||
await expect(mondaySegment).toBeVisible();
|
||||
|
||||
await page.getByText("Project view").click();
|
||||
await expect(page.getByText(/projects/)).toBeVisible();
|
||||
const projectRowSelector =
|
||||
`[data-testid="timeline-project-resource-row-canvas"][data-project-id="${scenario.projectId}"][data-resource-id="${scenario.resourceId}"]`;
|
||||
await switchToProjectView(page, projectRowSelector);
|
||||
|
||||
let mondayAssignment: { id: string; startDate: string; endDate: string } | null = null;
|
||||
await expect
|
||||
@@ -1551,9 +1956,7 @@ test.describe("Timeline", () => {
|
||||
})
|
||||
.not.toBeNull();
|
||||
|
||||
const projectRow = page.locator(
|
||||
`[data-testid="timeline-project-resource-row-canvas"][data-project-id="${scenario.projectId}"][data-resource-id="${scenario.resourceId}"]`,
|
||||
).first();
|
||||
const projectRow = page.locator(projectRowSelector).first();
|
||||
await expect(projectRow).toBeVisible();
|
||||
|
||||
const projectAllocation = projectRow.locator(
|
||||
@@ -1572,7 +1975,7 @@ test.describe("Timeline", () => {
|
||||
await popover.getByRole("button", { name: "Cancel" }).click();
|
||||
|
||||
await page.reload({ waitUntil: "domcontentloaded" });
|
||||
await page.getByText("Project view").click();
|
||||
await switchToProjectView(page, projectRowSelector);
|
||||
await expect(page.getByText(scenario.projectName).first()).toBeVisible();
|
||||
|
||||
const projectAllocationAfterReload = page
|
||||
@@ -1593,26 +1996,36 @@ test.describe("Timeline", () => {
|
||||
});
|
||||
|
||||
test("project view demand bars expose hover details", async ({ page }) => {
|
||||
await page.getByText("Project view").click();
|
||||
await expect(page.getByText(/projects/)).toBeVisible();
|
||||
await expect(page.locator("[data-timeline-entry-type='demand']").first()).toBeVisible();
|
||||
const suffix = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
||||
const scenario = createTimelineDemandScenario(suffix);
|
||||
|
||||
const demandId = await findVisibleTimelineEntryId(
|
||||
page,
|
||||
"[data-timeline-entry-type='demand'][data-allocation-id]",
|
||||
72,
|
||||
);
|
||||
expect(demandId).toBeTruthy();
|
||||
try {
|
||||
await page.goto(
|
||||
`/timeline?startDate=2026-04-01&days=31&projectIds=${scenario.projectId}`,
|
||||
{ waitUntil: "domcontentloaded" },
|
||||
);
|
||||
await ensureOpenDemandVisibilityEnabled(page);
|
||||
const demandRowSelector =
|
||||
`[data-project-demand-row="true"][data-project-id="${scenario.projectId}"]`;
|
||||
const demandSelector =
|
||||
`${demandRowSelector} [data-timeline-entry-type="demand"][data-allocation-id="${scenario.demandId}"]`;
|
||||
|
||||
const demandBar = page.locator(
|
||||
`[data-timeline-entry-type='demand'][data-allocation-id='${demandId}']`,
|
||||
);
|
||||
await demandBar.hover();
|
||||
await switchToProjectView(page, demandRowSelector);
|
||||
await expect(page.locator(demandSelector)).toBeVisible();
|
||||
|
||||
const demandTooltip = page.getByTestId("timeline-demand-tooltip");
|
||||
await expect(demandTooltip).toBeVisible();
|
||||
await expect(demandTooltip).toContainText("Requested");
|
||||
await expect(demandTooltip).toContainText("Open");
|
||||
await expect(demandTooltip).toContainText("Click for details and actions");
|
||||
const demandBar = page.locator(demandSelector);
|
||||
await demandBar.hover();
|
||||
|
||||
const demandTooltip = page.getByTestId("timeline-demand-tooltip");
|
||||
await expect(demandTooltip).toBeVisible();
|
||||
await expect(demandTooltip).toContainText("E2E Open Demand");
|
||||
await expect(demandTooltip).toContainText(scenario.projectShortCode);
|
||||
await expect(demandTooltip).toContainText("Requested");
|
||||
await expect(demandTooltip).toContainText("2 seats");
|
||||
await expect(demandTooltip).toContainText("Open");
|
||||
await expect(demandTooltip).toContainText("Click for details and actions");
|
||||
} finally {
|
||||
cleanupTimelineDemandScenario(scenario.projectId, scenario.resourceId);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user