Files
Endoleap/tests/feedback-scenario.mjs
T

69 lines
6.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 应用反馈组件的 JSDOM 场景:原生 title 回退、provider tooltip、通知更新及画布成功/错误提示。
* 只断言 DOM 属性、事件和状态,不证明真实屏幕阅读器播报或浏览器浮层排版。
*/
import assert from "node:assert/strict";
// 复用外层 React root,先验证独立控件,再验证带 AppFeedback 的画布交互。
export async function feedbackScenario({ root, act, h, AppFeedback, IconButton, notify, dismissNotification, GraphCanvas, window, makeCanvasNode, emptyCanvas, event, tick }) {
await act(async () => { root.render(null); await tick(); });
let button, clicks = 0;
// 最小 SVG 图标替身只满足 IconButton 接口,不依赖真实图标资源。
const icon = () => h("svg", { "aria-hidden": true });
// ref 必须直接指向业务 button;点击计数用于检查 tooltip 不吞掉原操作。
const control = () => h(IconButton, { icon, label: "旋转工具", title: "旋转选中元素", ref: (value) => { button = value; }, onClick: () => { clicks++; } });
// 没有 provider 时保留原生 title;加入 provider 后不增加包裹层,改用关联 tooltip。
await act(async () => { root.render(control()); await tick(); });
assert.equal(button.title, "旋转选中元素", "Standalone controls retain native explanations");
await act(async () => { root.render(h(AppFeedback, null, h("div", { id: "controls" }, control()))); await tick(); });
assert.ok(document.querySelector("#controls").firstElementChild === button, "Tooltip does not wrap the button or alter toolbar geometry");
assert.equal(button.tagName, "BUTTON", "Forwarded ref still points at the actual button");
assert.equal(button.title, "", "A provider avoids duplicate native and Radix tooltips");
// 键盘聚焦应出现说明并建立 aria-describedby,Escape 隐藏说明后点击仍能触发业务回调。
await act(async () => { button.focus(); await tick(); });
const tooltip = document.querySelector('[role="tooltip"]');
assert.ok(tooltip?.textContent.includes("旋转选中元素"), "Keyboard focus exposes the explanation");
assert.ok(button.getAttribute("aria-describedby"), "Explanation is associated with its trigger");
await event(document, "keydown", { key: "Escape" });
assert.ok(!document.querySelector('[role="tooltip"]'), "Escape dismisses the tooltip");
await event(button, "click"); assert.equal(clicks, 1, "Tooltips preserve business clicks");
// 同一通知 ID 更新内容,只保留一个 toast;随后显式 dismiss 并等待退出清理。
await act(async () => {
notify({ id: "feedback-check", title: "正在处理" }); await tick();
notify({ id: "feedback-check", title: "已完成", level: "success" }); await tick();
});
assert.equal(document.querySelectorAll('[data-sonner-toast]').length, 1, "One logical notice updates instead of duplicating");
assert.ok(document.querySelector('[data-sonner-toast]').textContent.includes("已完成"));
assert.ok(document.querySelector('[aria-label="关闭通知"]'), "Notification close control is labelled");
await act(async () => { dismissNotification("feedback-check"); await tick(); });
for (let i = 0; i < 20 && document.querySelector('[data-sonner-toast]'); i++) await act(async () => { await tick(); });
assert.ok(!document.querySelector('[data-sonner-toast]'), "Dismissed notifications are removed");
console.log("PASS feedback: native fallback, forwarded ref, keyboard tooltip/Escape, clicks and stable notification IDs");
// 播种旧版 demo 本地草稿,包含文本与分区;这不是服务端成员迁移测试。
const id = "feedback-board";
const node = { ...makeCanvasNode("text", { x: 100, y: 100 }), id: "feedback-text" };
const frame = { ...makeCanvasNode("frame", { x: 500, y: 100 }), id: "feedback-frame" };
frame.data.title = "提示分区";
localStorage.setItem(`endoleap_canvas_v1:w:${id}`, JSON.stringify({ ...emptyCanvas(), nodes: [node, frame] }));
await act(async () => { root.render(h(AppFeedback, null, h(GraphCanvas, { board: { id, workspace_id: "w", title: "反馈测试", kind: "board", version: 1 } }))); await tick(); });
// 拖入分区后同时核对成功提示和实际归属徽标,不能只看到 toast 就算操作生效。
const textNode = document.querySelector('.react-flow__node[data-id="feedback-text"]');
await event(textNode, "mousedown", { clientX: 200, clientY: 200, button: 0, buttons: 1 });
await event(window, "mousemove", { clientX: 210, clientY: 200, buttons: 1 });
await event(window, "mousemove", { clientX: 700, clientY: 200, buttons: 1 });
await event(window, "mouseup", { clientX: 700, clientY: 200, button: 0 });
assert.ok(document.querySelector('[data-sonner-toast][data-type="success"]')?.textContent.includes("已加入「提示分区」"));
assert.ok(document.querySelector('[title="属于:提示分区"]'), "Notifications do not replace actual membership state");
// 用 PDF MIME 模拟不支持的图片选择,错误通知出现后继续创建文本,检查错误不阻塞编辑。
const fileInput = document.querySelector('input[type="file"][accept^="image/"]');
Object.defineProperty(fileInput, "files", { configurable: true, value: [new File(["test"], "test.pdf", { type: "application/pdf" })] });
await act(async () => { fileInput.dispatchEvent(new window.Event("change", { bubbles: true })); await tick(); });
assert.ok(document.querySelector('[data-sonner-toast][data-type="error"]')?.textContent.includes("演示模式请选择"));
await event(document.querySelector('[aria-label="添加文本卡片"]'), "click");
assert.equal(document.querySelectorAll('.react-flow__node').length, 3, "An error notification never blocks editing");
// 关闭此画布错误通知并卸载组件,避免影响后续共享 root 的场景。
await act(async () => { dismissNotification(`canvas:${id}:error`); root.render(null); await tick(); });
console.log("PASS canvas feedback: membership success, input errors and continued local editing through the application provider");
}