265 lines
8.8 KiB
TypeScript
265 lines
8.8 KiB
TypeScript
import {
|
|
authFile,
|
|
createMedicationViaAPI,
|
|
deleteAllMedicationsViaAPI,
|
|
expect,
|
|
navigateTo,
|
|
type TestMedication,
|
|
test,
|
|
} from "./fixtures";
|
|
|
|
/**
|
|
* Tooltip Visibility Regression Tests
|
|
*
|
|
* Ensures that tooltip pseudo-elements on MedDetail footer icon buttons
|
|
* are not clipped by ancestor overflow or hidden behind modal overlays.
|
|
* This is a regression guard — tooltips have repeatedly broken due to
|
|
* CSS overflow/z-index changes on modal containers.
|
|
*/
|
|
test.describe("MedDetail footer tooltip visibility", () => {
|
|
test.use({ storageState: authFile });
|
|
test.describe.configure({ timeout: 60000 });
|
|
|
|
const MED_NAME = "Tooltip Test Med";
|
|
const createdMeds: TestMedication[] = [];
|
|
|
|
test.beforeAll(async () => {
|
|
await deleteAllMedicationsViaAPI();
|
|
createdMeds.push(
|
|
await createMedicationViaAPI({
|
|
name: MED_NAME,
|
|
packageType: "blister",
|
|
packCount: 1,
|
|
blistersPerPack: 1,
|
|
pillsPerBlister: 10,
|
|
looseTablets: 0,
|
|
intakes: [
|
|
{
|
|
usage: 1,
|
|
every: 1,
|
|
start: new Date().toISOString().slice(0, 16),
|
|
intakeRemindersEnabled: false,
|
|
},
|
|
],
|
|
})
|
|
);
|
|
});
|
|
|
|
test.afterAll(async () => {
|
|
await deleteAllMedicationsViaAPI();
|
|
});
|
|
|
|
/**
|
|
* Open the MedDetail modal by clicking a medication row in the Dashboard overview table.
|
|
*/
|
|
async function openMedDetailModal(page: import("@playwright/test").Page) {
|
|
await navigateTo(page, "/dashboard");
|
|
const overviewTable = page.locator(".dashboard-overview-section .table").first();
|
|
await expect(overviewTable).toBeVisible({ timeout: 10000 });
|
|
|
|
const medRow = overviewTable.locator(".table-row").filter({ hasText: MED_NAME }).first();
|
|
await medRow.click();
|
|
|
|
const modal = page.locator(".modal-overlay.med-detail-overlay");
|
|
await expect(modal).toBeVisible({ timeout: 5000 });
|
|
return modal;
|
|
}
|
|
|
|
test("no ancestor of footer tooltip buttons has overflow:hidden", async ({ page }) => {
|
|
const modal = await openMedDetailModal(page);
|
|
|
|
const footer = modal.locator(".med-detail-footer");
|
|
await expect(footer).toBeVisible();
|
|
|
|
// Walk up from footer through modal-content to modal-overlay and check overflow
|
|
const overflowHiddenAncestors = await page.evaluate(() => {
|
|
const footer = document.querySelector(".med-detail-footer");
|
|
if (!footer) return ["footer not found"];
|
|
|
|
const problems: string[] = [];
|
|
let el: HTMLElement | null = footer as HTMLElement;
|
|
while (el && !el.classList.contains("modal-overlay")) {
|
|
const computed = window.getComputedStyle(el);
|
|
const overflowX = computed.overflowX;
|
|
const overflowY = computed.overflowY;
|
|
if (overflowX === "hidden" || overflowY === "hidden") {
|
|
const id = el.id ? `#${el.id}` : "";
|
|
const cls = el.className ? `.${el.className.split(" ").join(".")}` : "";
|
|
problems.push(`${el.tagName.toLowerCase()}${id}${cls} has overflow: ${overflowX}/${overflowY}`);
|
|
}
|
|
el = el.parentElement;
|
|
}
|
|
return problems;
|
|
});
|
|
|
|
expect(
|
|
overflowHiddenAncestors,
|
|
`Tooltip ancestors must not clip with overflow:hidden: ${overflowHiddenAncestors.join("; ")}`
|
|
).toHaveLength(0);
|
|
});
|
|
|
|
test("tooltip z-index is above modal overlay", async ({ page }) => {
|
|
const _modal = await openMedDetailModal(page);
|
|
|
|
// Get modal overlay z-index and tooltip pseudo-element z-index from CSS
|
|
const { modalZIndex, tooltipZIndex, arrowZIndex } = await page.evaluate(() => {
|
|
const overlay = document.querySelector(".modal-overlay");
|
|
const overlayZ = overlay ? Number.parseInt(window.getComputedStyle(overlay).zIndex, 10) : 0;
|
|
|
|
// Read the tooltip ::after z-index from stylesheets
|
|
let ttZ = 0;
|
|
let arrZ = 0;
|
|
for (const sheet of document.styleSheets) {
|
|
try {
|
|
for (const rule of sheet.cssRules) {
|
|
const cssRule = rule as CSSStyleRule;
|
|
if (cssRule.selectorText?.includes("tooltip-trigger[data-tooltip]::after")) {
|
|
const z = Number.parseInt(cssRule.style.zIndex, 10);
|
|
if (z > ttZ) ttZ = z;
|
|
}
|
|
if (cssRule.selectorText?.includes("tooltip-trigger[data-tooltip]::before")) {
|
|
const z = Number.parseInt(cssRule.style.zIndex, 10);
|
|
if (z > arrZ) arrZ = z;
|
|
}
|
|
}
|
|
} catch {
|
|
// cross-origin sheets — skip
|
|
}
|
|
}
|
|
return { modalZIndex: overlayZ, tooltipZIndex: ttZ, arrowZIndex: arrZ };
|
|
});
|
|
|
|
expect(
|
|
tooltipZIndex,
|
|
`Tooltip ::after z-index (${tooltipZIndex}) must be > modal overlay z-index (${modalZIndex})`
|
|
).toBeGreaterThan(modalZIndex);
|
|
expect(
|
|
arrowZIndex,
|
|
`Tooltip ::before z-index (${arrowZIndex}) must be > modal overlay z-index (${modalZIndex})`
|
|
).toBeGreaterThan(modalZIndex);
|
|
});
|
|
|
|
test("edit button tooltip is visible on hover", async ({ page }) => {
|
|
const modal = await openMedDetailModal(page);
|
|
|
|
const editBtn = modal.locator(".med-detail-footer button.tooltip-trigger.info.icon-only");
|
|
await expect(editBtn).toBeVisible();
|
|
|
|
// Hover to activate tooltip
|
|
await editBtn.hover();
|
|
// Small wait for CSS transition
|
|
await page.waitForTimeout(300);
|
|
|
|
// Verify the tooltip pseudo-element is visible and within viewport
|
|
const isVisible = await page.evaluate(() => {
|
|
const btn = document.querySelector(".med-detail-footer button.tooltip-trigger.info.icon-only");
|
|
if (!btn) return { visible: false, reason: "button not found" };
|
|
|
|
const style = window.getComputedStyle(btn, "::after");
|
|
const opacity = Number.parseFloat(style.opacity);
|
|
const visibility = style.visibility;
|
|
|
|
if (opacity < 0.5 || visibility === "hidden") {
|
|
return {
|
|
visible: false,
|
|
reason: `opacity=${opacity}, visibility=${visibility}`,
|
|
};
|
|
}
|
|
return { visible: true, reason: "ok" };
|
|
});
|
|
|
|
expect(isVisible.visible, `Edit tooltip should be visible on hover: ${isVisible.reason}`).toBe(true);
|
|
});
|
|
|
|
test("stock correction button tooltip is visible on hover", async ({ page }) => {
|
|
const modal = await openMedDetailModal(page);
|
|
|
|
const stockBtn = modal.locator(".med-detail-footer button.tooltip-trigger.icon-stock-correction");
|
|
await expect(stockBtn).toBeVisible();
|
|
|
|
await stockBtn.hover();
|
|
await page.waitForTimeout(300);
|
|
|
|
const isVisible = await page.evaluate(() => {
|
|
const btn = document.querySelector(".med-detail-footer button.tooltip-trigger.icon-stock-correction");
|
|
if (!btn) return { visible: false, reason: "button not found" };
|
|
|
|
const style = window.getComputedStyle(btn, "::after");
|
|
const opacity = Number.parseFloat(style.opacity);
|
|
const visibility = style.visibility;
|
|
|
|
if (opacity < 0.5 || visibility === "hidden") {
|
|
return {
|
|
visible: false,
|
|
reason: `opacity=${opacity}, visibility=${visibility}`,
|
|
};
|
|
}
|
|
return { visible: true, reason: "ok" };
|
|
});
|
|
|
|
expect(isVisible.visible, `Stock correction tooltip should be visible on hover: ${isVisible.reason}`).toBe(true);
|
|
});
|
|
|
|
test("export button tooltip is visible on hover", async ({ page }) => {
|
|
const modal = await openMedDetailModal(page);
|
|
|
|
const exportBtn = modal.locator(".med-detail-footer button.tooltip-trigger.secondary.icon-only");
|
|
// Export button only shows when blisters exist — skip if not present
|
|
if (!(await exportBtn.isVisible().catch(() => false))) {
|
|
test.skip(true, "Export button not visible (no blisters)");
|
|
return;
|
|
}
|
|
|
|
await exportBtn.hover();
|
|
await page.waitForTimeout(300);
|
|
|
|
const isVisible = await page.evaluate(() => {
|
|
const btn = document.querySelector(".med-detail-footer button.tooltip-trigger.secondary.icon-only");
|
|
if (!btn) return { visible: false, reason: "button not found" };
|
|
|
|
const style = window.getComputedStyle(btn, "::after");
|
|
const opacity = Number.parseFloat(style.opacity);
|
|
const visibility = style.visibility;
|
|
|
|
if (opacity < 0.5 || visibility === "hidden") {
|
|
return {
|
|
visible: false,
|
|
reason: `opacity=${opacity}, visibility=${visibility}`,
|
|
};
|
|
}
|
|
return { visible: true, reason: "ok" };
|
|
});
|
|
|
|
expect(isVisible.visible, `Export tooltip should be visible on hover: ${isVisible.reason}`).toBe(true);
|
|
});
|
|
|
|
test("close button tooltip in header is visible on hover", async ({ page }) => {
|
|
const modal = await openMedDetailModal(page);
|
|
|
|
const closeBtn = modal.locator("button.modal-close.tooltip-trigger");
|
|
await expect(closeBtn).toBeVisible();
|
|
|
|
await closeBtn.hover();
|
|
await page.waitForTimeout(300);
|
|
|
|
const isVisible = await page.evaluate(() => {
|
|
const btn = document.querySelector(".med-detail-overlay button.modal-close.tooltip-trigger");
|
|
if (!btn) return { visible: false, reason: "button not found" };
|
|
|
|
const style = window.getComputedStyle(btn, "::after");
|
|
const opacity = Number.parseFloat(style.opacity);
|
|
const visibility = style.visibility;
|
|
|
|
if (opacity < 0.5 || visibility === "hidden") {
|
|
return {
|
|
visible: false,
|
|
reason: `opacity=${opacity}, visibility=${visibility}`,
|
|
};
|
|
}
|
|
return { visible: true, reason: "ok" };
|
|
});
|
|
|
|
expect(isVisible.visible, `Close button tooltip should be visible on hover: ${isVisible.reason}`).toBe(true);
|
|
});
|
|
});
|