Files
QOffice/src/features/qword/QWord.test.tsx
T
2026-06-02 09:01:20 +03:00

309 lines
12 KiB
TypeScript

import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { useState } from "react";
import { afterEach, describe, expect, it } from "vitest";
import { vi } from "vitest";
import type { WordDocument } from "../../types";
import { QWord, qwordCurrentPageFromViewport } from "./QWord";
afterEach(() => {
cleanup();
});
describe("qwordCurrentPageFromViewport", () => {
it("вычисляет текущую страницу по положению листа в области просмотра", () => {
expect(qwordCurrentPageFromViewport({ stageTop: 34, paperTop: 227, pageHeight: 1123, pageCount: 3, zoom: 1 })).toBe(1);
expect(qwordCurrentPageFromViewport({ stageTop: 34, paperTop: -1090, pageHeight: 1123, pageCount: 3, zoom: 1 })).toBe(2);
expect(qwordCurrentPageFromViewport({ stageTop: 34, paperTop: -6000, pageHeight: 1123, pageCount: 3, zoom: 1 })).toBe(3);
});
it("учитывает масштаб при расчете текущей страницы", () => {
expect(qwordCurrentPageFromViewport({ stageTop: 34, paperTop: -1200, pageHeight: 1123, pageCount: 3, zoom: 1 })).toBe(2);
expect(qwordCurrentPageFromViewport({ stageTop: 34, paperTop: -1200, pageHeight: 1123, pageCount: 3, zoom: 1.2 })).toBe(1);
});
});
describe("QWord", () => {
it("переключает вкладки ленты и показывает команды выбранной вкладки", () => {
const originalResizeObserver = globalThis.ResizeObserver;
const originalRequestAnimationFrame = window.requestAnimationFrame;
globalThis.ResizeObserver = class {
observe() {}
disconnect() {}
} as unknown as typeof ResizeObserver;
window.requestAnimationFrame = ((callback: FrameRequestCallback) => {
callback(0);
return 1;
}) as typeof window.requestAnimationFrame;
const document: WordDocument = {
id: "word-ribbon-tabs-test",
title: "Лента.docx",
updatedAt: "2026-06-02T00:00:00.000Z",
html: "<p>Текст</p>"
};
try {
render(<QWord document={document} onChange={vi.fn()} />);
expect(screen.getByRole("tab", { name: "Главная" })).toHaveAttribute("aria-selected", "true");
expect(screen.getByRole("region", { name: "Шрифт" })).toBeInTheDocument();
fireEvent.click(screen.getByRole("tab", { name: "Вставка" }));
expect(screen.getByRole("tab", { name: "Вставка" })).toHaveAttribute("aria-selected", "true");
expect(screen.getByRole("region", { name: "Вставка" })).toBeInTheDocument();
expect(screen.queryByRole("region", { name: "Шрифт" })).not.toBeInTheDocument();
fireEvent.click(screen.getByRole("tab", { name: "Файл" }));
expect(screen.getByRole("tab", { name: "Файл" })).toHaveAttribute("aria-selected", "true");
expect(screen.getByRole("region", { name: "Файл" })).toBeInTheDocument();
expect(screen.queryByRole("region", { name: "Вставка" })).not.toBeInTheDocument();
} finally {
globalThis.ResizeObserver = originalResizeObserver;
window.requestAnimationFrame = originalRequestAnimationFrame;
}
});
it("переключает режим просмотра документа на вкладке Вид", () => {
const originalResizeObserver = globalThis.ResizeObserver;
const originalRequestAnimationFrame = window.requestAnimationFrame;
globalThis.ResizeObserver = class {
observe() {}
disconnect() {}
} as unknown as typeof ResizeObserver;
window.requestAnimationFrame = ((callback: FrameRequestCallback) => {
callback(0);
return 1;
}) as typeof window.requestAnimationFrame;
const document: WordDocument = {
id: "word-view-mode-test",
title: "Вид.docx",
updatedAt: "2026-06-02T00:00:00.000Z",
html: "<p>Текст</p>"
};
try {
render(<QWord document={document} onChange={vi.fn()} />);
const stage = screen.getByLabelText("Редактор QWord");
expect(stage).toHaveClass("qword-view-print");
fireEvent.click(screen.getByRole("tab", { name: "Вид" }));
fireEvent.click(screen.getByRole("button", { name: "Веб-документ" }));
expect(stage).toHaveClass("qword-view-web");
expect(screen.getByRole("button", { name: "Веб-документ" })).toHaveAttribute("aria-pressed", "true");
} finally {
globalThis.ResizeObserver = originalResizeObserver;
window.requestAnimationFrame = originalRequestAnimationFrame;
}
});
it("вставляет таблицу выбранного размера через вкладку Вставка", () => {
const originalResizeObserver = globalThis.ResizeObserver;
const originalRequestAnimationFrame = window.requestAnimationFrame;
const onChange = vi.fn();
globalThis.ResizeObserver = class {
observe() {}
disconnect() {}
} as unknown as typeof ResizeObserver;
window.requestAnimationFrame = ((callback: FrameRequestCallback) => {
callback(0);
return 1;
}) as typeof window.requestAnimationFrame;
const document: WordDocument = {
id: "word-insert-table-test",
title: "Таблица.docx",
updatedAt: "2026-06-02T00:00:00.000Z",
html: "<p>Перед таблицей</p>"
};
try {
render(<QWord document={document} onChange={onChange} />);
fireEvent.click(screen.getByRole("tab", { name: "Вставка" }));
fireEvent.change(screen.getByLabelText("Строки таблицы"), { target: { value: "2" } });
fireEvent.change(screen.getByLabelText("Столбцы таблицы"), { target: { value: "3" } });
fireEvent.click(screen.getByRole("button", { name: "Таблица" }));
const updatedDocument = onChange.mock.calls[onChange.mock.calls.length - 1]?.[0] as WordDocument | undefined;
expect(updatedDocument?.html).toContain("<table>");
expect(updatedDocument?.html.match(/<tr/g)).toHaveLength(2);
expect(updatedDocument?.html.match(/<td/g)).toHaveLength(6);
} finally {
globalThis.ResizeObserver = originalResizeObserver;
window.requestAnimationFrame = originalRequestAnimationFrame;
}
});
it("сохраняет активную ячейку после вставки таблицы и перерендера", () => {
const originalResizeObserver = globalThis.ResizeObserver;
const originalRequestAnimationFrame = window.requestAnimationFrame;
const onChange = vi.fn();
globalThis.ResizeObserver = class {
observe() {}
disconnect() {}
} as unknown as typeof ResizeObserver;
window.requestAnimationFrame = ((callback: FrameRequestCallback) => {
callback(0);
return 1;
}) as typeof window.requestAnimationFrame;
const document: WordDocument = {
id: "word-insert-table-restore-test",
title: "Активная таблица.docx",
updatedAt: "2026-06-02T00:00:00.000Z",
html: "<p>Перед таблицей</p>"
};
function StatefulQWord() {
const [currentDocument, setCurrentDocument] = useState(document);
return (
<QWord
document={currentDocument}
onChange={(nextDocument) => {
onChange(nextDocument);
setCurrentDocument(nextDocument);
}}
/>
);
}
try {
render(<StatefulQWord />);
fireEvent.click(screen.getByRole("tab", { name: "Вставка" }));
const tableButton = screen.getByRole("button", { name: "Таблица" });
expect(fireEvent.mouseDown(tableButton)).toBe(false);
fireEvent.click(tableButton);
const rowButton = screen.getByRole("button", { name: "Строка ниже" });
expect(fireEvent.mouseDown(rowButton)).toBe(false);
fireEvent.click(rowButton);
const updatedDocument = onChange.mock.calls[onChange.mock.calls.length - 1]?.[0] as WordDocument | undefined;
expect(updatedDocument?.html.match(/<tr/g)).toHaveLength(4);
expect(updatedDocument?.html.match(/<td/g)).toHaveLength(12);
} finally {
globalThis.ResizeObserver = originalResizeObserver;
window.requestAnimationFrame = originalRequestAnimationFrame;
}
});
it("добавляет строку и столбец к выбранной таблице", () => {
const originalResizeObserver = globalThis.ResizeObserver;
const originalRequestAnimationFrame = window.requestAnimationFrame;
const onChange = vi.fn();
globalThis.ResizeObserver = class {
observe() {}
disconnect() {}
} as unknown as typeof ResizeObserver;
window.requestAnimationFrame = ((callback: FrameRequestCallback) => {
callback(0);
return 1;
}) as typeof window.requestAnimationFrame;
const document: WordDocument = {
id: "word-table-structure-test",
title: "Структура таблицы.docx",
updatedAt: "2026-06-02T00:00:00.000Z",
html: "<table><tbody><tr><td>A</td><td>B</td></tr></tbody></table>"
};
function selectCell(text: string) {
const cell = screen.getByText(text).closest("td");
expect(cell).toBeInTheDocument();
const range = window.document.createRange();
range.selectNodeContents(cell as HTMLTableCellElement);
window.getSelection()?.removeAllRanges();
window.getSelection()?.addRange(range);
}
function StatefulQWord() {
const [currentDocument, setCurrentDocument] = useState(document);
return (
<QWord
document={currentDocument}
onChange={(nextDocument) => {
onChange(nextDocument);
setCurrentDocument(nextDocument);
}}
/>
);
}
try {
render(<StatefulQWord />);
fireEvent.click(screen.getByRole("tab", { name: "Вставка" }));
selectCell("A");
const rowButton = screen.getByRole("button", { name: "Строка ниже" });
expect(fireEvent.mouseDown(rowButton)).toBe(false);
fireEvent.click(rowButton);
let updatedDocument = onChange.mock.calls[onChange.mock.calls.length - 1]?.[0] as WordDocument | undefined;
expect(updatedDocument?.html.match(/<tr/g)).toHaveLength(2);
expect(updatedDocument?.html.match(/<td/g)).toHaveLength(4);
selectCell("A");
const columnButton = screen.getByRole("button", { name: "Столбец справа" });
expect(fireEvent.mouseDown(columnButton)).toBe(false);
fireEvent.click(columnButton);
updatedDocument = onChange.mock.calls[onChange.mock.calls.length - 1]?.[0] as WordDocument | undefined;
expect(updatedDocument?.html.match(/<tr/g)).toHaveLength(2);
expect(updatedDocument?.html.match(/<td/g)).toHaveLength(6);
} finally {
globalThis.ResizeObserver = originalResizeObserver;
window.requestAnimationFrame = originalRequestAnimationFrame;
}
});
it("обновляет текущую страницу в статусе при прокрутке документа", async () => {
const originalResizeObserver = globalThis.ResizeObserver;
const originalRequestAnimationFrame = window.requestAnimationFrame;
const onStatusChange = vi.fn();
globalThis.ResizeObserver = class {
observe() {}
disconnect() {}
} as unknown as typeof ResizeObserver;
window.requestAnimationFrame = ((callback: FrameRequestCallback) => {
callback(0);
return 1;
}) as typeof window.requestAnimationFrame;
const document: WordDocument = {
id: "word-scroll-test",
title: "Прокрутка.docx",
updatedAt: "2026-06-02T00:00:00.000Z",
html: "<p>Первая страница</p>"
};
try {
const view = render(<QWord document={document} onChange={vi.fn()} onStatusChange={onStatusChange} />);
const stage = view.getByLabelText("Редактор QWord");
const paper = view.getByLabelText("Содержимое документа");
Object.defineProperty(paper, "scrollHeight", { configurable: true, value: 2400 });
stage.getBoundingClientRect = () => ({ bottom: 884, height: 850, left: 0, right: 1008, top: 34, width: 1008, x: 0, y: 34, toJSON: () => ({}) });
paper.getBoundingClientRect = () => ({ bottom: -77, height: 1123, left: 107, right: 901, top: -1200, width: 794, x: 107, y: -1200, toJSON: () => ({}) });
fireEvent.scroll(stage);
await waitFor(() => expect(onStatusChange).toHaveBeenLastCalledWith({ currentPage: 2, pageCount: 3, wordCount: 2 }));
} finally {
globalThis.ResizeObserver = originalResizeObserver;
window.requestAnimationFrame = originalRequestAnimationFrame;
}
});
});