309 lines
12 KiB
TypeScript
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;
|
|
}
|
|
});
|
|
});
|