fix: harden favorite row picker
This commit is contained in:
@@ -50,6 +50,44 @@ function readPopover(): HTMLElement | null {
|
||||
return document.querySelector('[data-sces-favorite-row-picker="root"]');
|
||||
}
|
||||
|
||||
function createDeferred(): {
|
||||
promise: Promise<void>;
|
||||
resolve: () => void;
|
||||
} {
|
||||
let resolve!: () => void;
|
||||
const promise = new Promise<void>((nextResolve) => {
|
||||
resolve = nextResolve;
|
||||
});
|
||||
return { promise, resolve };
|
||||
}
|
||||
|
||||
function createRect(options: {
|
||||
bottom?: number;
|
||||
height?: number;
|
||||
left?: number;
|
||||
top?: number;
|
||||
width?: number;
|
||||
}): DOMRect {
|
||||
const bottom = options.bottom ?? (options.top ?? 0) + (options.height ?? 0);
|
||||
const height = options.height ?? bottom - (options.top ?? 0);
|
||||
const left = options.left ?? 0;
|
||||
const top = options.top ?? bottom - height;
|
||||
const width = options.width ?? 0;
|
||||
return {
|
||||
bottom,
|
||||
height,
|
||||
left,
|
||||
right: left + width,
|
||||
toJSON() {
|
||||
return {};
|
||||
},
|
||||
top,
|
||||
width,
|
||||
x: left,
|
||||
y: top
|
||||
} as DOMRect;
|
||||
}
|
||||
|
||||
function sync(options: {
|
||||
actionCell?: HTMLElement;
|
||||
authorId?: string;
|
||||
@@ -208,6 +246,50 @@ describe("favorite-row-picker", () => {
|
||||
expect(readFavoriteButton(actionCell).dataset.scesFavoriteCount).toBe("2");
|
||||
});
|
||||
|
||||
test("retains a confirmed save through a resync while it is pending", async () => {
|
||||
const actionCell = createActionCell();
|
||||
const deferredSave = createDeferred();
|
||||
const onSetCreatorFolderIds = vi.fn(() => deferredSave.promise);
|
||||
|
||||
sync({ actionCell, onSetCreatorFolderIds });
|
||||
readFavoriteButton(actionCell).click();
|
||||
const firstCheckbox = document.querySelector(
|
||||
`input[data-sces-favorite-folder-id="${firstFolder.id}"]`
|
||||
) as HTMLInputElement;
|
||||
firstCheckbox.checked = true;
|
||||
firstCheckbox.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
|
||||
sync({ actionCell, onSetCreatorFolderIds });
|
||||
expect(readFavoriteButton(actionCell).disabled).toBe(true);
|
||||
expect(
|
||||
(
|
||||
document.querySelector(
|
||||
`input[data-sces-favorite-folder-id="${firstFolder.id}"]`
|
||||
) as HTMLInputElement
|
||||
).disabled
|
||||
).toBe(true);
|
||||
|
||||
deferredSave.resolve();
|
||||
await flushAsyncEvents();
|
||||
|
||||
expect(readFavoriteButton(actionCell).disabled).toBe(false);
|
||||
expect(readFavoriteButton(actionCell).dataset.scesFavoriteCount).toBe("1");
|
||||
expect(
|
||||
(
|
||||
document.querySelector(
|
||||
`input[data-sces-favorite-folder-id="${firstFolder.id}"]`
|
||||
) as HTMLInputElement
|
||||
).checked
|
||||
).toBe(true);
|
||||
expect(
|
||||
(
|
||||
document.querySelector(
|
||||
`input[data-sces-favorite-folder-id="${firstFolder.id}"]`
|
||||
) as HTMLInputElement
|
||||
).disabled
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("adds a newly created folder to the creator selection", async () => {
|
||||
const actionCell = createActionCell();
|
||||
const createdFolder = createFolder("folder-3", "重点");
|
||||
@@ -273,6 +355,90 @@ describe("favorite-row-picker", () => {
|
||||
expect(readPopover()).toBeNull();
|
||||
});
|
||||
|
||||
test("clamps a bottom-right picker within the viewport", () => {
|
||||
const actionCell = createActionCell();
|
||||
const originalWidth = Object.getOwnPropertyDescriptor(
|
||||
document.documentElement,
|
||||
"clientWidth"
|
||||
);
|
||||
const originalHeight = Object.getOwnPropertyDescriptor(
|
||||
document.documentElement,
|
||||
"clientHeight"
|
||||
);
|
||||
Object.defineProperty(document.documentElement, "clientWidth", {
|
||||
configurable: true,
|
||||
value: 300
|
||||
});
|
||||
Object.defineProperty(document.documentElement, "clientHeight", {
|
||||
configurable: true,
|
||||
value: 200
|
||||
});
|
||||
const rectSpy = vi
|
||||
.spyOn(HTMLElement.prototype, "getBoundingClientRect")
|
||||
.mockImplementation(function mockPickerRect(this: HTMLElement): DOMRect {
|
||||
if (this.dataset.scesFavoriteRowAction === "button") {
|
||||
return createRect({ bottom: 190, height: 20, left: 280, top: 170, width: 20 });
|
||||
}
|
||||
if (this.dataset.scesFavoriteRowPicker === "root") {
|
||||
return createRect({ height: 60, width: 120 });
|
||||
}
|
||||
return createRect({});
|
||||
});
|
||||
|
||||
try {
|
||||
sync({ actionCell });
|
||||
readFavoriteButton(actionCell).click();
|
||||
|
||||
const root = readPopover() as HTMLElement;
|
||||
expect(Number.parseFloat(root.style.left)).toBeGreaterThanOrEqual(8);
|
||||
expect(Number.parseFloat(root.style.left)).toBeLessThanOrEqual(172);
|
||||
expect(Number.parseFloat(root.style.top)).toBeGreaterThanOrEqual(8);
|
||||
expect(Number.parseFloat(root.style.top)).toBeLessThanOrEqual(132);
|
||||
expect(root.style.top).toBe("102px");
|
||||
} finally {
|
||||
rectSpy.mockRestore();
|
||||
if (originalWidth) {
|
||||
Object.defineProperty(document.documentElement, "clientWidth", originalWidth);
|
||||
} else {
|
||||
delete (document.documentElement as { clientWidth?: number }).clientWidth;
|
||||
}
|
||||
if (originalHeight) {
|
||||
Object.defineProperty(document.documentElement, "clientHeight", originalHeight);
|
||||
} else {
|
||||
delete (document.documentElement as { clientHeight?: number }).clientHeight;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("sets trigger dialog state, focuses the picker, and restores focus on Escape", () => {
|
||||
const actionCell = createActionCell();
|
||||
sync({ actionCell });
|
||||
|
||||
const button = readFavoriteButton(actionCell);
|
||||
expect(button.getAttribute("aria-haspopup")).toBe("dialog");
|
||||
expect(button.getAttribute("aria-expanded")).toBe("false");
|
||||
const pickerId = button.getAttribute("aria-controls");
|
||||
expect(pickerId).not.toBeNull();
|
||||
|
||||
button.focus();
|
||||
button.click();
|
||||
const root = readPopover() as HTMLElement;
|
||||
expect(root.id).toBe(pickerId);
|
||||
expect(root.getAttribute("role")).toBe("dialog");
|
||||
expect(root.getAttribute("aria-label")).toBe("选择收藏夹");
|
||||
expect(button.getAttribute("aria-expanded")).toBe("true");
|
||||
expect(document.activeElement).toBe(
|
||||
document.querySelector(
|
||||
`input[data-sces-favorite-folder-id="${firstFolder.id}"]`
|
||||
)
|
||||
);
|
||||
|
||||
document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" }));
|
||||
expect(readPopover()).toBeNull();
|
||||
expect(button.getAttribute("aria-expanded")).toBe("false");
|
||||
expect(document.activeElement).toBe(button);
|
||||
});
|
||||
|
||||
test("resyncs one control with fresh callbacks and closes an obsolete popover", async () => {
|
||||
const actionCell = createActionCell();
|
||||
const staleCallback = vi.fn(async () => {});
|
||||
|
||||
Reference in New Issue
Block a user