// The documentation items against Java's documentation mode // (documentation.java.json; tools/java-oracle's DocuRun on JDocuElement): // a new text and rectangle, the colours and font sizes Java's menus offer, // the font and colour chosen there, the smallest sizes a drag leaves, the // stacking, and a project Java saved with documentation (fixtures/ // documentation.fls) read, kept in the document and written again. import { readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; import * as Y from 'yjs'; import { DOCUMENTATION_FONT_SIZES, DOCUMENTATION_RECTANGLE_COLORS, DOCUMENTATION_TEXT_COLORS, documentationFontStyle, documentationItem, inJavaDrawingOrder, resizedFrame, type DocumentationColor, type DocumentationItem, } from '@mdg-filius/core'; import { exportProject, openProjectArchive } from '@mdg-filius/fls'; import { addDocumentation, documentationText, readProject, removeDocumentation, setDocumentationColor, setDocumentationFont, setDocumentationFrame, topologyView, writeProject } from '../src/index.js'; type Rgba = [number, number, number, number]; interface JavaFont { readonly name: string; readonly style: number; readonly size: number; } interface JavaElement { readonly bounds: [number, number, number, number]; readonly color: Rgba; readonly font: JavaFont | null; readonly text: string | null; } interface JavaItem extends JavaElement { readonly type: number; } interface JavaMenuEntry { readonly menu?: string; readonly items?: string[]; readonly colors?: string; readonly list?: Rgba[]; readonly rows?: number; readonly item?: string; readonly separator?: boolean; } const here = dirname(fileURLToPath(import.meta.url)); const java = JSON.parse(readFileSync(join(here, 'documentation.java.json'), 'utf8')) as { readonly new: { readonly text: JavaElement; readonly rectangle: JavaElement }; readonly fonts: JavaFont[]; readonly textColor: Rgba; readonly rectangleColor: Rgba; readonly menus: { readonly text: JavaMenuEntry[]; readonly rectangle: JavaMenuEntry[] }; readonly smallest: Record; readonly stacking: string[]; readonly topAt: string; readonly saved: JavaItem[]; readonly loaded: JavaItem[]; }; const channel = (value: number) => Math.round(value * 255); const rgba = (color: DocumentationColor): Rgba => [channel(color.red), channel(color.green), channel(color.blue), channel(color.alpha)]; /** An item as Java's GUIDocuItem shows it: its bounds, colour and font (none for a rectangle). */ const asJava = (item: DocumentationItem) => ({ bounds: [item.frame.x, item.frame.y, item.frame.width, item.frame.height], color: rgba(item.color), font: item.kind === 'text' ? { name: item.fontName, style: documentationFontStyle(item), size: item.fontSize } : null, text: item.kind === 'text' ? item.text : null, }); const itemOf = (doc: Y.Doc, id: string) => readProject(doc).documentationItems.find((item) => item.id === id)!; describe("the documentation items, against Java's documentation mode", () => { it('makes a new text and rectangle as Java does: 100 × 50 where dropped, a text empty and black in Dialog 12, a rectangle grey', () => { const doc = new Y.Doc(); addDocumentation(doc, { id: 'text', kind: 'text', x: 100, y: 100 }, 'me'); addDocumentation(doc, { id: 'rectangle', kind: 'rectangle', x: 300.4, y: 99.6 }, 'me'); expect(asJava(itemOf(doc, 'text'))).toEqual(java.new.text); expect(asJava(itemOf(doc, 'rectangle'))).toEqual(java.new.rectangle); // After the others: of its kind the lowest. expect([itemOf(doc, 'text').order, itemOf(doc, 'rectangle').order]).toEqual([0, 1]); // An item made without a font has Java's: Dialog 12, plain. expect(documentationItem({ id: 'x', kind: 'text', frame: { x: 0, y: 0, width: 5, height: 5 }, color: DOCUMENTATION_TEXT_COLORS[0]!, order: 0 })).toMatchObject({ fontName: 'Dialog', fontSize: 12, isBold: false, isItalic: false, frame: { x: 0, y: 0, width: 5, height: 5 }, }); }); it("offers Java's colours and font sizes, and sets font and colour as Java's menu does", () => { const colors = (entries: JavaMenuEntry[]) => entries.find((entry) => entry.list !== undefined)!; expect(DOCUMENTATION_TEXT_COLORS.map(rgba)).toEqual(colors(java.menus.text).list); expect(DOCUMENTATION_RECTANGLE_COLORS.map(rgba)).toEqual(colors(java.menus.rectangle).list); expect([colors(java.menus.text).rows, colors(java.menus.rectangle).rows]).toEqual([3, 3]); expect(java.menus.text.map((entry) => entry.menu ?? entry.colors ?? entry.item ?? '-')).toEqual(['Schriftgröße', 'Schriftstil', 'Farbe', '-', 'Löschen']); expect(java.menus.rectangle.map((entry) => entry.menu ?? entry.colors ?? entry.item ?? '-')).toEqual(['Farbe', '-', 'Löschen']); // „Groß“, „Kursiv“, „Fett“, „Normal“, „Sehr Groß“: a style replaces the one before. const doc = new Y.Doc(); addDocumentation(doc, { id: 'text', kind: 'text', x: 100, y: 100 }, 'me'); const font = () => asJava(itemOf(doc, 'text')).font; const fonts = []; for (const change of [{ size: DOCUMENTATION_FONT_SIZES[1] }, { style: 'italic' }, { style: 'bold' }, { size: DOCUMENTATION_FONT_SIZES[0] }, { size: DOCUMENTATION_FONT_SIZES[2] }] as const) { setDocumentationFont(doc, 'text', change, 'me'); fonts.push(font()); } expect(fonts).toEqual(java.fonts); // The drawing knows the style too. setDocumentationFont(doc, 'text', { style: 'italic' }, 'me'); expect(topologyView(doc).docs.map((item) => [item.bold, item.italic])).toEqual([[false, true]]); setDocumentationFont(doc, 'text', { style: 'plain' }, 'me'); expect(font()?.style).toBe(0); setDocumentationColor(doc, 'text', DOCUMENTATION_TEXT_COLORS[7]!, 'me'); expect(rgba(itemOf(doc, 'text').color)).toEqual(java.textColor); // A rectangle takes a colour, not a font. addDocumentation(doc, { id: 'rectangle', kind: 'rectangle', x: 300, y: 100 }, 'me'); setDocumentationFont(doc, 'rectangle', { size: 24, style: 'bold' }, 'me'); setDocumentationColor(doc, 'rectangle', DOCUMENTATION_RECTANGLE_COLORS[4]!, 'me'); expect(asJava(itemOf(doc, 'rectangle'))).toMatchObject({ color: java.rectangleColor, font: null }); expect([itemOf(doc, 'rectangle').fontSize, itemOf(doc, 'rectangle').isBold]).toEqual([12, false]); }); it('leaves a rectangle at least 20 × 20 and a text at least 88 × 20, the opposite edge where it was', () => { const rectangle = { x: 300, y: 100, width: 100, height: 50 }; const [x, y, width, height] = java.smallest.rectangle!.bounds; expect(resizedFrame(rectangle, 'rectangle', { right: true, bottom: true }, { x: 303, y: 103 })).toEqual({ x, y, width, height }); // By its top left corner, far past the other: the right and lower edges stay. const small = { x: 300, y: 100, width: 20, height: 20 }; const [ax, ay, aw, ah] = java.smallest.rectangleByItsTopLeft!.bounds; expect(resizedFrame(small, 'rectangle', { left: true, top: true }, { x: 500, y: 300 })).toEqual({ x: ax, y: ay, width: aw, height: ah }); const [tx, ty, tw, th] = java.smallest.textSmallest!.bounds; expect(resizedFrame({ x: 100, y: 100, width: 100, height: 50 }, 'text', { right: true, bottom: true }, { x: 0, y: 0 })).toEqual({ x: tx, y: ty, width: tw, height: th }); // Edges follow the pointer; the others stay. expect(resizedFrame(rectangle, 'rectangle', { left: true }, { x: 250, y: 0 })).toEqual({ x: 250, y: 100, width: 150, height: 50 }); expect(resizedFrame(rectangle, 'rectangle', { top: true }, { x: 0, y: 130 })).toEqual({ x: 300, y: 130, width: 100, height: 20 }); expect(resizedFrame(rectangle, 'rectangle', { bottom: true }, { x: 999, y: 300 })).toEqual({ x: 300, y: 100, width: 100, height: 200 }); expect(resizedFrame(rectangle, 'rectangle', {}, { x: 0, y: 0 })).toEqual(rectangle); // The document keeps whole pixels. const doc = new Y.Doc(); addDocumentation(doc, { id: 'r', kind: 'rectangle', x: 0, y: 0 }, 'me'); setDocumentationFrame(doc, 'r', { x: 10.4, y: 20.6, width: 30.5, height: 40.2 }, 'me'); expect(itemOf(doc, 'r').frame).toEqual({ x: 10, y: 21, width: 31, height: 40 }); }); it('stacks as Java does: texts over rectangles, of one kind the earlier over the later', () => { const doc = new Y.Doc(); const names = ['text 1', 'rectangle 1', 'text 2', 'rectangle 2']; names.forEach((name, index) => addDocumentation(doc, { id: name, kind: name.startsWith('text') ? 'text' : 'rectangle', x: 50 + 10 * index, y: 50 + 10 * index }, 'me')); // Drawn in this order, the last on top; Java names the top one first. const drawn = topologyView(doc).docs.map((item) => item.id); expect([...drawn].reverse()).toEqual(java.stacking); expect(drawn.at(-1)).toBe(java.topAt); expect(inJavaDrawingOrder(readProject(doc).documentationItems).map((item) => item.id)).toEqual(drawn); }); it('reads what Java saved, keeps it in the document, and writes it as Java does', () => { let counter = 0; const project = openProjectArchive(new Uint8Array(readFileSync(join(here, 'fixtures/documentation.fls'))), () => `id-${++counter}`); expect(java.loaded).toEqual(java.saved); const byFile = (items: readonly DocumentationItem[]) => [...items].sort((lhs, rhs) => lhs.order - rhs.order); const javaItems = java.loaded.map(({ type, ...rest }) => ({ ...rest, type })); expect(byFile(project.documentationItems).map((item) => ({ ...asJava(item), type: item.kind === 'rectangle' ? 1 : 2 }))).toEqual(javaItems); // Through the document and back: italic and all. const doc = new Y.Doc(); writeProject(doc, project); expect(readProject(doc).documentationItems).toEqual(project.documentationItems); expect(documentationText(doc, project.documentationItems.find((item) => item.kind === 'text')!.id)).toBeInstanceOf(Y.Text); expect(documentationText(doc, project.documentationItems.find((item) => item.kind === 'rectangle')!.id)).toBeUndefined(); // Written as Java writes it (rectangles first in the list, which leaves Java's stacking of each kind as it was). const written = exportProject(readProject(doc)).topology.documentation.map((item) => ({ ...item, color: item.color === null ? null : [...item.color], font: item.font === null ? null : { ...item.font }, })); const expected = [...java.saved.filter((item) => item.type === 1), ...java.saved.filter((item) => item.type === 2)].map((item) => ({ type: item.type, text: item.text ?? '', x: item.bounds[0], y: item.bounds[1], width: item.bounds[2], height: item.bounds[3], color: item.color, // A rectangle has no font in Java; the model gives every item one. font: item.font ?? { name: 'Dialog', style: 0, size: 12 }, })); expect(written).toEqual(expected); // A colour of Java's menu is opaque, also over a translucent one from a file. const translucent = project.documentationItems[0]!; setDocumentationColor(doc, translucent.id, { ...translucent.color, alpha: 0.5 }, 'me'); expect(itemOf(doc, translucent.id).color.alpha).toBe(0.5); setDocumentationColor(doc, translucent.id, DOCUMENTATION_RECTANGLE_COLORS[4]!, 'me'); expect(rgba(itemOf(doc, translucent.id).color)).toEqual([255, 201, 201, 255]); removeDocumentation(doc, project.documentationItems[0]!.id, 'me'); expect(readProject(doc).documentationItems).toHaveLength(3); }); });