




















@@ -388,6 +388,56 @@ function openDeleteConfirm(deleteButton: HTMLButtonElement) {
388388deleteButton.dispatchEvent(new MouseEvent("click", { bubbles: true }));
389389}
390390391+function domRect(params: {
392+left?: number;
393+top?: number;
394+width?: number;
395+height?: number;
396+}): DOMRect {
397+const left = params.left ?? 0;
398+const top = params.top ?? 0;
399+const width = params.width ?? 0;
400+const height = params.height ?? 0;
401+const rect = {
402+x: left,
403+y: top,
404+ left,
405+ top,
406+ width,
407+ height,
408+right: left + width,
409+bottom: top + height,
410+toJSON: () => rect,
411+};
412+return rect as DOMRect;
413+}
414+415+function stubDeleteConfirmGeometry(params: {
416+trigger: { left: number; top: number; width: number; height: number };
417+popover: { width: number; height: number };
418+viewport: { left?: number; top?: number; width: number; height: number };
419+}) {
420+vi.stubGlobal("innerWidth", params.viewport.width);
421+vi.stubGlobal("innerHeight", params.viewport.height);
422+vi.stubGlobal("visualViewport", {
423+height: params.viewport.height,
424+offsetLeft: params.viewport.left ?? 0,
425+offsetTop: params.viewport.top ?? 0,
426+width: params.viewport.width,
427+});
428+vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockImplementation(
429+function (this: HTMLElement) {
430+if (this.classList.contains("chat-group-delete")) {
431+return domRect(params.trigger);
432+}
433+if (this.classList.contains("chat-delete-confirm")) {
434+return domRect(params.popover);
435+}
436+return domRect({});
437+},
438+);
439+}
440+391441function clickDeleteButtonIconPath(deleteButton: HTMLButtonElement) {
392442const icon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
393443const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
@@ -569,6 +619,68 @@ describe("grouped chat rendering", () => {
569619]);
570620});
571621622+it("places the delete confirm below the trigger near the top viewport edge", () => {
623+stubDeleteConfirmGeometry({
624+trigger: { left: 20, top: 4, width: 24, height: 24 },
625+popover: { width: 200, height: 96 },
626+viewport: { width: 320, height: 240 },
627+});
628+const fixture = renderDeleteConfirmFixture();
629+630+openDeleteConfirm(fixture.deleteButton);
631+632+const popover = expectElement(fixture.container, ".chat-delete-confirm", HTMLElement);
633+expect(popover.dataset.placement).toBe("below");
634+expect(popover.style.top).toBe("34px");
635+expect(popover.style.left).toBe("20px");
636+});
637+638+it("places the delete confirm above the trigger near the bottom viewport edge", () => {
639+stubDeleteConfirmGeometry({
640+trigger: { left: 20, top: 190, width: 24, height: 24 },
641+popover: { width: 200, height: 80 },
642+viewport: { width: 320, height: 240 },
643+});
644+const fixture = renderDeleteConfirmFixture();
645+646+openDeleteConfirm(fixture.deleteButton);
647+648+const popover = expectElement(fixture.container, ".chat-delete-confirm", HTMLElement);
649+expect(popover.dataset.placement).toBe("above");
650+expect(popover.style.top).toBe("104px");
651+expect(popover.style.left).toBe("20px");
652+});
653+654+it("clamps the delete confirm horizontally inside narrow viewports", () => {
655+stubDeleteConfirmGeometry({
656+trigger: { left: 260, top: 120, width: 24, height: 24 },
657+popover: { width: 200, height: 80 },
658+viewport: { width: 320, height: 240 },
659+});
660+const fixture = renderDeleteConfirmFixture();
661+662+openDeleteConfirm(fixture.deleteButton);
663+664+const popover = expectElement(fixture.container, ".chat-delete-confirm", HTMLElement);
665+expect(popover.style.left).toBe("112px");
666+});
667+668+it("clamps the delete confirm inside shifted visual viewports", () => {
669+stubDeleteConfirmGeometry({
670+trigger: { left: 620, top: 540, width: 24, height: 24 },
671+popover: { width: 200, height: 80 },
672+viewport: { left: 320, top: 300, width: 320, height: 240 },
673+});
674+const fixture = renderDeleteConfirmFixture();
675+676+openDeleteConfirm(fixture.deleteButton);
677+678+const popover = expectElement(fixture.container, ".chat-delete-confirm", HTMLElement);
679+expect(popover.dataset.placement).toBe("above");
680+expect(popover.style.left).toBe("432px");
681+expect(popover.style.top).toBe("452px");
682+});
683+572684it("removes the delete confirm outside-click listener when Cancel dismisses it", () => {
573685const fixture = setupArmedDeleteConfirm();
574686const cancel = fixture.container.querySelector<HTMLButtonElement>(
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。