feat: menu contextuel et selection type Excel dans la grille XLSX #155
This commit is contained in:
@@ -179,6 +179,16 @@ const clickDialog = (which) => {
|
||||
btn.click();
|
||||
};
|
||||
|
||||
/** #155 — open the grid context menu on a cell (right-click). */
|
||||
const openMenuOn = (area, ref) => {
|
||||
const td = area.querySelector(`td[data-cell="${ref}"]`);
|
||||
assert.ok(td, `cell ${ref} exists`);
|
||||
td.dispatchEvent(new w.MouseEvent("contextmenu", { bubbles: true, clientX: 12, clientY: 12 }));
|
||||
return document.querySelector(".xlsx-context-menu");
|
||||
};
|
||||
const menuItem = (label) =>
|
||||
[...document.querySelectorAll(".xlsx-context-item")].find((b) => b.textContent === label);
|
||||
|
||||
const lossyError = {
|
||||
ok: false,
|
||||
status: 409,
|
||||
@@ -559,14 +569,16 @@ const mountGrid = () => {
|
||||
return area;
|
||||
};
|
||||
|
||||
await test("clicking a header sorts the rows numerically or lexically", () => {
|
||||
await test("the context menu sorts the rows numerically or lexically", () => {
|
||||
const area = mountGrid();
|
||||
// Sort by column B (numbers) ascending: 2, 3, 10.
|
||||
area.querySelector(".xlsx-table thead th:nth-child(3)").click();
|
||||
openMenuOn(area, "B1");
|
||||
menuItem(FR["xlsx.sort_asc"]).click();
|
||||
let cells = [...area.querySelectorAll("tbody td[data-cell^=\"B\"]")].map((td) => td.textContent);
|
||||
assert.deepEqual(cells, ["2", "3", "10"]);
|
||||
// Second click: descending.
|
||||
area.querySelector(".xlsx-table thead th:nth-child(3)").click();
|
||||
// Re-open and sort descending.
|
||||
openMenuOn(area, "B1");
|
||||
menuItem(FR["xlsx.sort_desc"]).click();
|
||||
cells = [...area.querySelectorAll("tbody td[data-cell^=\"B\"]")].map((td) => td.textContent);
|
||||
assert.deepEqual(cells, ["10", "3", "2"]);
|
||||
});
|
||||
@@ -574,7 +586,8 @@ await test("clicking a header sorts the rows numerically or lexically", () => {
|
||||
await test("a dirty cell travels with its row during a sort", () => {
|
||||
const area = mountGrid();
|
||||
editCell(area, "A3", "Cerise modifiée");
|
||||
area.querySelector(".xlsx-table thead th:nth-child(3)").click();
|
||||
openMenuOn(area, "B1");
|
||||
menuItem(FR["xlsx.sort_asc"]).click();
|
||||
const aCells = [...area.querySelectorAll("tbody td[data-cell^=\"A\"]")].map((td) => td.textContent);
|
||||
assert.ok(aCells.includes("Cerise modifiée"), aCells.join("|"));
|
||||
assert.equal(area.querySelectorAll("td.xlsx-dirty").length, 1);
|
||||
@@ -963,6 +976,87 @@ await test("the inspector exposes a resize handle", () => {
|
||||
assert.ok(area.querySelector("#xlsx-inspector-resize"), "resize handle present");
|
||||
});
|
||||
|
||||
// ── #155 — selection & context menu ─────────────────────────────────────────
|
||||
|
||||
await test("a right-click on a cell opens a themed context menu", () => {
|
||||
const area = mount();
|
||||
const menu = openMenuOn(area, "A1");
|
||||
assert.ok(menu, "menu rendered");
|
||||
assert.ok(menuItem(FR["xlsx.insert_row_above"]), "insert row item");
|
||||
assert.ok(menuItem(FR["xlsx.sort_asc"]), "sort item");
|
||||
assert.ok(menuItem(FR["xlsx.clear_contents"]), "clear item");
|
||||
});
|
||||
|
||||
await test("the context menu inserts a row through the structure endpoint", async () => {
|
||||
const area = mount();
|
||||
apiQueue.push({ ok: true, status: 200, body: { status: "ok" } }); // PUT
|
||||
apiQueue.push({
|
||||
ok: true, status: 200,
|
||||
body: { is_xlsx: true, vault: "V", path: "data.xlsx", xlsx_sheets: [{ name: "Feuille1", html: sheetHtml("100") }], xlsx_lossy_features: [] },
|
||||
}); // re-read
|
||||
openMenuOn(area, "A2");
|
||||
menuItem(FR["xlsx.insert_row_above"]).click();
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
assert.equal(calls.length, 2);
|
||||
assert.match(calls[0].url, /\/xlsx\/structure\?path=data\.xlsx/);
|
||||
assert.deepEqual(calls[0].body.actions, [{ op: "row_insert", sheet: "Feuille1", at: 2, count: 1 }]);
|
||||
});
|
||||
|
||||
await test("clear contents empties the selected cells (local edit)", () => {
|
||||
const area = mount();
|
||||
openMenuOn(area, "A1");
|
||||
menuItem(FR["xlsx.clear_contents"]).click();
|
||||
assert.equal(area.querySelector('td[data-cell="A1"]').textContent, "");
|
||||
assert.equal(area.querySelectorAll("td.xlsx-dirty").length, 1);
|
||||
assert.equal(area.querySelector("#xlsx-save-btn").disabled, false);
|
||||
});
|
||||
|
||||
await test("clicking a column header selects the whole column", () => {
|
||||
const area = mount();
|
||||
area.querySelector(".xlsx-table thead th:nth-child(2)").click(); // column A
|
||||
assert.equal(area.querySelector("#xlsx-active-cell").textContent, "A1:A2");
|
||||
const selected = [...area.querySelectorAll("td.xlsx-selected")].map((td) => td.dataset.cell).sort();
|
||||
assert.deepEqual(selected, ["A1", "A2"]);
|
||||
});
|
||||
|
||||
await test("clicking a row header selects the whole row", () => {
|
||||
const area = mount();
|
||||
area.querySelector("th.xlsx-rownum").click(); // row 1
|
||||
assert.equal(area.querySelector("#xlsx-active-cell").textContent, "A1:B1");
|
||||
});
|
||||
|
||||
await test("dragging from a cell to another selects the range", () => {
|
||||
const area = mount();
|
||||
const viewer = area.querySelector(".xlsx-viewer");
|
||||
const a1 = area.querySelector('td[data-cell="A1"]');
|
||||
const b2 = area.querySelector('td[data-cell="B2"]');
|
||||
a1.dispatchEvent(new w.MouseEvent("mousedown", { bubbles: true, button: 0 }));
|
||||
b2.dispatchEvent(new w.MouseEvent("mouseover", { bubbles: true, button: 0 }));
|
||||
viewer.dispatchEvent(new w.MouseEvent("mouseup", { bubbles: true }));
|
||||
assert.equal(area.querySelector("#xlsx-active-cell").textContent, "A1:B2");
|
||||
assert.equal(area.querySelectorAll("td.xlsx-selected").length, 4);
|
||||
});
|
||||
|
||||
await test("Shift+Arrow extends the selection from the anchor", () => {
|
||||
const area = mount();
|
||||
const a1 = area.querySelector('td[data-cell="A1"]');
|
||||
a1.focus();
|
||||
a1.dispatchEvent(new w.KeyboardEvent("keydown", { key: "ArrowRight", shiftKey: true, bubbles: true }));
|
||||
assert.equal(area.querySelector("#xlsx-active-cell").textContent, "A1:B1");
|
||||
assert.equal(area.querySelectorAll("td.xlsx-selected").length, 2);
|
||||
});
|
||||
|
||||
await test("the context menu module exposes open/close helpers", async () => {
|
||||
const mod = await import(
|
||||
pathToFileURL(path.join(REPO_ROOT, "frontend", "js", "xlsx", "context-menu.js")).href
|
||||
);
|
||||
const menu = mod.openContextMenu(5, 5, [{ label: "Only" }]);
|
||||
assert.equal(menu.querySelectorAll(".xlsx-context-item").length, 1);
|
||||
assert.equal(mod.isContextMenuOpen(), true);
|
||||
mod.closeContextMenu();
|
||||
assert.equal(mod.isContextMenuOpen(), false);
|
||||
});
|
||||
|
||||
// ── Report ──────────────────────────────────────────────────────────────────
|
||||
console.log(`\n${passCount}/${testCount} tests passed\n`);
|
||||
process.exit(passCount === testCount ? 0 : 1);
|
||||
|
||||
Reference in New Issue
Block a user