178 lines
8.8 KiB
TypeScript
178 lines
8.8 KiB
TypeScript
// @vitest-environment happy-dom
|
|
|
|
import { act, useEffect, useState } from "react";
|
|
import { createRoot, type Root } from "react-dom/client";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
import { ExtraSheetEditor } from "./extra-sheet-editor";
|
|
import { createExtraSheet, mergeExtraSheetRange, type ExtraSheet } from "@/lib/guides/extra-sheet";
|
|
|
|
let root: Root;
|
|
let container: HTMLDivElement;
|
|
let latest: ExtraSheet;
|
|
let sequence = 0;
|
|
const createId = (prefix: string) => `${prefix}-${++sequence}`;
|
|
|
|
function Harness({ initial }: { initial: ExtraSheet }) {
|
|
const [sheet, setSheet] = useState(initial);
|
|
useEffect(() => { latest = sheet; }, [sheet]);
|
|
return <ExtraSheetEditor value={sheet} createId={createId} onChange={setSheet} onRemove={() => {}} />;
|
|
}
|
|
|
|
async function mouseDown(element: Element, shiftKey = false) {
|
|
await act(async () => {
|
|
element.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, shiftKey }));
|
|
});
|
|
}
|
|
|
|
async function click(element: Element) {
|
|
await mouseDown(element);
|
|
await act(async () => { element.dispatchEvent(new MouseEvent("click", { bubbles: true })); });
|
|
}
|
|
|
|
function cell(row: number, column: number) {
|
|
return container.querySelector<HTMLTableCellElement>(`[data-sheet-row="${row}"][data-sheet-column="${column}"]`)!;
|
|
}
|
|
|
|
function button(label: string) {
|
|
return Array.from(container.querySelectorAll("button")).find((item) => item.getAttribute("aria-label") === label || item.textContent?.trim() === label)!;
|
|
}
|
|
|
|
async function typeText(element: HTMLTextAreaElement, text: string) {
|
|
await act(async () => {
|
|
Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, "value")!.set!.call(element, text);
|
|
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
});
|
|
}
|
|
|
|
async function mount(initial = createExtraSheet(createId)) {
|
|
await act(async () => { root.render(<Harness initial={initial} />); });
|
|
}
|
|
|
|
beforeEach(() => {
|
|
sequence = 0;
|
|
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
// Supply grid measurements; happy-dom does not run browser layout.
|
|
vi.spyOn(HTMLElement.prototype, "offsetWidth", "get").mockImplementation(function (this: HTMLElement) { return this.tagName === "TD" ? 140 : 1000; });
|
|
vi.spyOn(HTMLElement.prototype, "offsetHeight", "get").mockImplementation(function (this: HTMLElement) { return this.tagName === "TD" ? 36 : 180; });
|
|
vi.spyOn(HTMLElement.prototype, "offsetTop", "get").mockImplementation(function (this: HTMLElement) { return (Number(this.dataset.sheetRow ?? -1) + 1) * 36; });
|
|
vi.spyOn(HTMLElement.prototype, "offsetLeft", "get").mockImplementation(function (this: HTMLElement) { return (Number(this.dataset.sheetColumn ?? -1) + 1) * 140; });
|
|
container = document.createElement("div");
|
|
document.body.append(container);
|
|
root = createRoot(container);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await act(async () => { root.unmount(); });
|
|
container.remove();
|
|
vi.restoreAllMocks();
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
describe("extra spreadsheet interactions", () => {
|
|
it("saves inline text when another cell is clicked without Enter", async () => {
|
|
await mount();
|
|
await mouseDown(cell(0, 0));
|
|
await click(container.querySelector(".Spreadsheet__active-cell--view")!);
|
|
const input = container.querySelector<HTMLTextAreaElement>(".Spreadsheet__data-editor textarea")!;
|
|
expect(input).not.toBeNull();
|
|
await typeText(input, "Keep this text");
|
|
await mouseDown(cell(1, 1));
|
|
expect(latest.rows[0].cells[latest.columns[0].id]).toBe("<p>Keep this text</p>");
|
|
expect(latest.rows[1].cells[latest.columns[1].id]).toBe("");
|
|
});
|
|
|
|
it("saves inline text on blur and cancels Escape", async () => {
|
|
await mount();
|
|
await mouseDown(cell(0, 0));
|
|
await click(container.querySelector(".Spreadsheet__active-cell--view")!);
|
|
let input = container.querySelector<HTMLTextAreaElement>(".Spreadsheet__data-editor textarea")!;
|
|
await typeText(input, "Saved on blur");
|
|
await act(async () => { input.blur(); });
|
|
expect(latest.rows[0].cells[latest.columns[0].id]).toBe("<p>Saved on blur</p>");
|
|
await mouseDown(cell(0, 1));
|
|
await click(container.querySelector(".Spreadsheet__active-cell--view")!);
|
|
input = container.querySelector<HTMLTextAreaElement>(".Spreadsheet__data-editor textarea")!;
|
|
await typeText(input, "Discard");
|
|
await act(async () => { input.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true })); });
|
|
expect(latest.rows[0].cells[latest.columns[1].id]).toBe("");
|
|
});
|
|
|
|
it.each(["เลือกสีพื้นหลังเซลล์", "เลือกสีตัวอักษร"])("keeps %s on the selection where its picker opened", async (label) => {
|
|
const sheet = createExtraSheet(createId);
|
|
sheet.rows[0].cells[sheet.columns[0].id] = "<p>First</p>";
|
|
sheet.rows[1].cells[sheet.columns[1].id] = "<p>Second</p>";
|
|
await mount(sheet);
|
|
await mouseDown(cell(0, 0));
|
|
const picker = container.querySelector<HTMLInputElement>(`input[aria-label="${label}"]`)!;
|
|
await act(async () => { picker.dispatchEvent(new Event("pointerdown", { bubbles: true })); });
|
|
await mouseDown(cell(1, 1));
|
|
await act(async () => {
|
|
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")!.set!.call(picker, "#123456");
|
|
picker.dispatchEvent(new Event("change", { bubbles: true }));
|
|
});
|
|
if (label === "เลือกสีพื้นหลังเซลล์") {
|
|
expect(latest.rows[0].backgrounds[latest.columns[0].id]).toBe("#123456");
|
|
expect(latest.rows[1].backgrounds).toEqual({});
|
|
} else {
|
|
expect(latest.rows[0].cells[latest.columns[0].id]).toContain("#123456");
|
|
expect(latest.rows[1].cells[latest.columns[1].id]).toBe("<p>Second</p>");
|
|
}
|
|
});
|
|
|
|
it("merges a selection, supports undo/redo, and unmerges", async () => {
|
|
const sheet = createExtraSheet(createId);
|
|
sheet.rows[0].cells[sheet.columns[0].id] = "<p>First</p>";
|
|
sheet.rows[1].cells[sheet.columns[1].id] = "<p>Last</p>";
|
|
await mount(sheet);
|
|
await mouseDown(cell(0, 0));
|
|
await mouseDown(cell(1, 1), true);
|
|
expect(button("ผสานเซลล์").disabled).toBe(false);
|
|
await click(button("ผสานเซลล์"));
|
|
expect(latest.merges).toHaveLength(1);
|
|
expect(latest.rows[0].cells[latest.columns[0].id]).toBe("<p>First</p><p>Last</p>");
|
|
expect(cell(0, 0).getAttribute("aria-rowspan")).toBe("2");
|
|
expect(cell(1, 1).getAttribute("aria-hidden")).toBe("true");
|
|
await click(button("ย้อนกลับการแก้ไขตาราง"));
|
|
expect(latest.merges).toBeUndefined();
|
|
expect(latest.rows[1].cells[latest.columns[1].id]).toBe("<p>Last</p>");
|
|
await click(button("ทำซ้ำการแก้ไขตาราง"));
|
|
expect(latest.merges).toHaveLength(1);
|
|
await click(button("ยกเลิกผสาน"));
|
|
expect(latest.merges).toEqual([]);
|
|
});
|
|
|
|
it.each(["Enter", "Tab"])("commits merged inline edits and skips coverage with %s", async (key) => {
|
|
const merged = mergeExtraSheetRange(createExtraSheet(createId), { start: { row: 0, column: 0 }, end: { row: 1, column: 1 } });
|
|
await mount(merged);
|
|
await mouseDown(cell(0, 0));
|
|
await click(container.querySelector(".Spreadsheet__active-cell--view")!);
|
|
const input = container.querySelector<HTMLTextAreaElement>(".Spreadsheet__data-editor textarea")!;
|
|
await typeText(input, "Committed with key");
|
|
await act(async () => {
|
|
input.dispatchEvent(new KeyboardEvent("keydown", { key, bubbles: true }));
|
|
await new Promise((resolve) => requestAnimationFrame(resolve));
|
|
});
|
|
expect(latest.rows[0].cells[latest.columns[0].id]).toBe("<p>Committed with key</p>");
|
|
const expected = key === "Enter" ? cell(2, 0) : cell(0, 2);
|
|
const overlay = container.querySelector<HTMLElement>(".Spreadsheet__active-cell--view")!;
|
|
expect(overlay.style.top).toBe(`${expected.offsetTop}px`);
|
|
expect(overlay.style.left).toBe(`${expected.offsetLeft}px`);
|
|
expect(document.activeElement).toBe(overlay);
|
|
});
|
|
|
|
it("activates merge anchors, skips covered cells, and persists middle alignment", async () => {
|
|
const merged = mergeExtraSheetRange(createExtraSheet(createId), { start: { row: 0, column: 0 }, end: { row: 1, column: 1 } });
|
|
await mount(merged);
|
|
await mouseDown(cell(1, 1));
|
|
await click(button("กึ่งกลางแนวตั้ง"));
|
|
expect(latest.rows[0].verticalAlignments[latest.columns[0].id]).toBe("middle");
|
|
expect(cell(0, 0).querySelector<HTMLElement>(".extra-sheet-merged-cell")?.style.justifyContent).toBe("center");
|
|
await act(async () => {
|
|
cell(0, 0).dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowRight", bubbles: true }));
|
|
await new Promise((resolve) => requestAnimationFrame(resolve));
|
|
});
|
|
expect(container.querySelector(".Spreadsheet__active-cell")?.getAttribute("style")).toContain("left: 420px");
|
|
});
|
|
});
|