Files
mdg-filius/packages/doc/test/documentation.test.ts
julian.kiedaischandClaude Opus 5.5 56cdb4926e feat: F4.8a the documentation items in model and file, measured against Java's
New oracle DocuRun on Java's JDocuElement. Italic is kept (the model knew
only bold), an item keeps its file's size and font size (Swift's 32 x 24
and 8-72 changed Java's files), an item without a colour is Java's light
grey. Java's new items, colour lists, font sizes and styles, the drag's
smallest sizes and Java's stacking; document commands for the editor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 10:42:21 +00:00

209 lines
11 KiB
TypeScript
Raw Permalink 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.
// 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<string, JavaElement>;
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);
});
});