importProject carries the system software settings into the model the core runs: switch SSID and learning time (an SSID Java would roll comes from the node id, as in Swift), a host's wireless network, manual routes, RIP and the firewall of every router and home router. The Swift oracle writes these settings too; they match on the 13 files Swift reads completely. A router's default gateway — its first interface's, which Java routes to (Weiterleitungstabelle.holeTabelle) — is imported into the router's device configuration. Swift's model drops it; in internetwork_komplett the way back from 141.99.5.10 depends on it. A test pings across three routers in the core, with and without it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
508 lines
20 KiB
TypeScript
508 lines
20 KiB
TypeScript
// The editor model built from a .fls file must be what the Swift import
|
|
// builds (corpus.swift.json, `state`, tools/swift-oracle) — on every corpus
|
|
// file Swift reads completely. On the others Swift loses devices (plan §13,
|
|
// M2); there Java is the reference, checked in topology.test.ts.
|
|
|
|
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
import { dirname, join, relative } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
defaultDisplayName,
|
|
defaultSwitchConfiguration,
|
|
firewallConfiguration,
|
|
FIREWALL_PROTOCOL,
|
|
interfaceKey,
|
|
deepEqual,
|
|
NetworkRuntimeEngine,
|
|
portSnapshot,
|
|
topologySnapshot,
|
|
type Id,
|
|
type NodeKind,
|
|
} from '@mdg-filius/core';
|
|
import {
|
|
decodeJavaBeans,
|
|
importProject,
|
|
readFlsArchive,
|
|
readTopology,
|
|
type FlsNode,
|
|
type FlsTopology,
|
|
type ImportedProject,
|
|
} from '../src/index.js';
|
|
|
|
const corpusRoot = join(dirname(fileURLToPath(import.meta.url)), '../../../corpus');
|
|
interface SwiftSummary {
|
|
skipped?: number;
|
|
warnings?: string[];
|
|
state?: unknown;
|
|
}
|
|
const swiftView = JSON.parse(readFileSync(join(corpusRoot, 'corpus.swift.json'), 'utf8')) as Record<string, SwiftSummary>;
|
|
|
|
function flsFiles(directory: string): string[] {
|
|
return readdirSync(directory)
|
|
.flatMap((name) => {
|
|
const path = join(directory, name);
|
|
return statSync(path).isDirectory() ? flsFiles(path) : name.endsWith('.fls') ? [path] : [];
|
|
})
|
|
.sort();
|
|
}
|
|
|
|
/** Ids a test can read: node-1, node-2, … in the order they are made. */
|
|
function counter(): () => Id {
|
|
let next = 0;
|
|
return () => `00000000-0000-0000-0000-${String(++next).padStart(12, '0')}`;
|
|
}
|
|
|
|
function importFile(file: string): ImportedProject {
|
|
const values = decodeJavaBeans(readFlsArchive(new Uint8Array(readFileSync(file))).configurationXml);
|
|
return importProject(readTopology(values), counter());
|
|
}
|
|
|
|
/**
|
|
* describeSystem's shape. A firewall as the core has it without one
|
|
* (inactive, drop, no rules) is left out: Swift keeps a configuration only
|
|
* when the file sets a field, and the core treats both alike.
|
|
*/
|
|
function systemOf(project: ImportedProject, nodeId: Id) {
|
|
const result: Record<string, unknown> = {};
|
|
const switchConfiguration = project.switchConfigurations.get(nodeId);
|
|
if (switchConfiguration !== undefined) {
|
|
result.switch = {
|
|
ssid: switchConfiguration.ssid === defaultSwitchConfiguration(nodeId).ssid ? '<from node id>' : switchConfiguration.ssid,
|
|
retentionTimeMilliseconds: switchConfiguration.retentionTimeMilliseconds,
|
|
};
|
|
}
|
|
const wireless = project.hostWirelessConfigurations.get(nodeId);
|
|
if (wireless !== undefined) result.hostWireless = wireless;
|
|
const routes = project.manualRoutesByNodeId.get(nodeId);
|
|
if (routes !== undefined) {
|
|
result.manualRoutes = routes.map((route) => [route.destinationNetwork, route.subnetMask, route.gateway, route.interfaceIPAddress]);
|
|
}
|
|
const rip = project.ripEnabledByNodeId.get(nodeId);
|
|
if (rip !== undefined) result.ripEnabled = rip;
|
|
const firewall = project.firewallConfigurationsByNodeId.get(nodeId);
|
|
if (firewall !== undefined && !deepEqual(firewall, firewallConfiguration())) {
|
|
result.firewall = {
|
|
isActive: firewall.isActive,
|
|
defaultPolicy: firewall.defaultPolicy === 'accept' ? 1 : 0,
|
|
dropICMP: firewall.dropIcmp,
|
|
filterSYNSegmentsOnly: firewall.filterSynSegmentsOnly,
|
|
filterUDP: firewall.filterUdp,
|
|
rules: firewall.rules.map((rule) => [
|
|
rule.sourceIPAddress,
|
|
rule.sourceSubnetMask,
|
|
rule.destinationIPAddress,
|
|
rule.destinationSubnetMask,
|
|
rule.port,
|
|
FIREWALL_PROTOCOL[rule.protocol],
|
|
rule.action === 'accept' ? 1 : 0,
|
|
]),
|
|
};
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/** The shape describeImportedState (tools/swift-oracle) writes: indices instead of random ids. */
|
|
function asSwiftState(project: ImportedProject) {
|
|
const { nodes, links } = project.graph;
|
|
const nodeIndex = new Map(nodes.map((node, index) => [node.id, index]));
|
|
const portIndex = (nodeId: Id, portId: Id) => nodes[nodeIndex.get(nodeId)!]!.ports.findIndex((port) => port.id === portId);
|
|
return {
|
|
nodes: nodes.map((node) => ({
|
|
kind: node.kind,
|
|
displayName: node.displayName,
|
|
hostLabelPolicy: node.hostLabelPolicy,
|
|
position: [node.position.x, node.position.y],
|
|
ports: node.ports.map((port) => ({ label: port.label, mac: port.importedMacAddress ?? null, occupied: port.isOccupied })),
|
|
device: project.deviceConfigurations.get(node.id) ?? null,
|
|
interfaces: node.ports.map((port) => project.interfaceConfigurations.get(interfaceKey(node.id, port.id)) ?? null),
|
|
system: systemOf(project, node.id),
|
|
})),
|
|
links: links.map((link) => [
|
|
nodeIndex.get(link.sourceNodeId),
|
|
portIndex(link.sourceNodeId, link.sourcePortId),
|
|
nodeIndex.get(link.targetNodeId),
|
|
portIndex(link.targetNodeId, link.targetPortId),
|
|
]),
|
|
documentation: project.documentationItems.map((item) => ({
|
|
id: item.id,
|
|
kind: item.kind,
|
|
frame: [item.frame.x, item.frame.y, item.frame.width, item.frame.height],
|
|
text: item.text,
|
|
color: [item.color.red, item.color.green, item.color.blue, item.color.alpha],
|
|
fontName: item.fontName,
|
|
fontSize: item.fontSize,
|
|
isBold: item.isBold,
|
|
order: item.order,
|
|
})),
|
|
};
|
|
}
|
|
|
|
/** Java's name for a device the file leaves unnamed (German FILIUS), where Swift uses its own. */
|
|
const JAVA_DEFAULT_NAMES: Partial<Record<NodeKind, string>> = {
|
|
networkSwitch: 'Switch / WLAN',
|
|
gateway: 'Heimrouter',
|
|
};
|
|
|
|
/**
|
|
* Swift's state with the one intended difference applied: a node the file
|
|
* leaves unnamed has Java's default name, not Swift's ("Switch / WLAN", not
|
|
* "Switch"). A node the file names like Swift's default would fail here, not
|
|
* pass unnoticed.
|
|
*/
|
|
function withJavaDefaultNames(state: unknown): unknown {
|
|
const { nodes, ...rest } = state as { nodes: { kind: NodeKind; displayName: string }[] };
|
|
return {
|
|
...rest,
|
|
nodes: nodes.map((node) =>
|
|
node.displayName === defaultDisplayName(node.kind) && JAVA_DEFAULT_NAMES[node.kind] !== undefined
|
|
? { ...node, displayName: JAVA_DEFAULT_NAMES[node.kind] }
|
|
: node,
|
|
),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Where the Swift import is wrong and Java (corpus.java.json) is right, per
|
|
* file and node: the corrected fields.
|
|
*/
|
|
const SWIFT_ERRORS: Record<string, Record<number, { displayName: string; hostLabelPolicy: string }>> = {
|
|
// useIPAsName is `<object idref="Boolean0"/>`, the shared result of a
|
|
// button's isEnabled() — true. Swift's reader cannot follow it: it shows
|
|
// the label text as a manual name instead of the IP address.
|
|
'java-examples/multicast_server.fls': {
|
|
0: { displayName: 'Rechner', hostLabelPolicy: 'ipAddress' },
|
|
1: { displayName: 'Notebook', hostLabelPolicy: 'ipAddress' },
|
|
2: { displayName: 'Notebook', hostLabelPolicy: 'ipAddress' },
|
|
},
|
|
};
|
|
|
|
function withSwiftErrorsCorrected(name: string, state: unknown): unknown {
|
|
const corrections = SWIFT_ERRORS[name] ?? {};
|
|
const { nodes, ...rest } = state as { nodes: object[] };
|
|
return { ...rest, nodes: nodes.map((node, index) => ({ ...node, ...corrections[index] })) };
|
|
}
|
|
|
|
const complete = (summary: SwiftSummary) =>
|
|
summary.skipped === 0 && !(summary.warnings ?? []).some((warning) => warning.startsWith('Skipped'));
|
|
|
|
describe('corpus: the imported model as Swift builds it', () => {
|
|
const files = flsFiles(corpusRoot).filter((file) => complete(swiftView[relative(corpusRoot, file)]!));
|
|
|
|
it('has files to compare', () => {
|
|
expect(files.length).toBe(13);
|
|
});
|
|
|
|
for (const file of files) {
|
|
const name = relative(corpusRoot, file);
|
|
it(name, () => {
|
|
const project = importFile(file);
|
|
|
|
expect(asSwiftState(project)).toEqual(withSwiftErrorsCorrected(name, withJavaDefaultNames(swiftView[name]!.state)));
|
|
expect(project.warnings).toEqual([]);
|
|
});
|
|
}
|
|
});
|
|
|
|
describe('corpus: every file imports completely', () => {
|
|
const javaView = JSON.parse(readFileSync(join(corpusRoot, 'corpus.java.json'), 'utf8')) as Record<
|
|
string,
|
|
{ nodes: unknown[]; cables: unknown[]; documentation: unknown[] }
|
|
>;
|
|
|
|
// Also the files where Swift loses devices: as many devices and cables as
|
|
// Java has, and nothing left out.
|
|
for (const file of flsFiles(corpusRoot)) {
|
|
const name = relative(corpusRoot, file);
|
|
it(name, () => {
|
|
const project = importFile(file);
|
|
|
|
expect(project.graph.nodes).toHaveLength(javaView[name]!.nodes.length);
|
|
expect(project.graph.links).toHaveLength(javaView[name]!.cables.length);
|
|
expect(project.warnings).toEqual([]);
|
|
});
|
|
}
|
|
});
|
|
|
|
/** The core started on an imported project (the editor will derive this snapshot from its state). */
|
|
function startedEngine(project: ImportedProject): NetworkRuntimeEngine {
|
|
const engine = new NetworkRuntimeEngine(14);
|
|
engine.handle({
|
|
type: 'start',
|
|
snapshot: topologySnapshot({
|
|
nodes: project.graph.nodes.map((node) => ({
|
|
id: node.id,
|
|
kind: node.kind,
|
|
ports: node.ports.map((port) => portSnapshot(port.id, port.label, port.importedMacAddress)),
|
|
})),
|
|
links: project.graph.links,
|
|
deviceConfigurations: project.deviceConfigurations,
|
|
interfaceConfigurations: project.interfaceConfigurations,
|
|
manualRoutesByNodeId: project.manualRoutesByNodeId,
|
|
ripEnabledByNodeId: project.ripEnabledByNodeId,
|
|
firewallConfigurationsByNodeId: project.firewallConfigurationsByNodeId,
|
|
switchConfigurationsByNodeId: project.switchConfigurations,
|
|
}),
|
|
seed: 14,
|
|
initialTimeMilliseconds: 0,
|
|
});
|
|
return engine;
|
|
}
|
|
|
|
describe('an imported file runs in the simulation core', () => {
|
|
it('pings across the router from the notebook with a gateway, not from the one without', () => {
|
|
const project = importFile(join(corpusRoot, 'java-examples/zwei_rechnernetze_komplett.fls'));
|
|
const byName = (displayName: string) => project.graph.nodes.find((node) => node.displayName === displayName)!.id;
|
|
const engine = startedEngine(project);
|
|
|
|
expect(engine.sendIcmpEcho(byName('Notebook 1.10'), '141.99.2.10').type).toBe('delivered');
|
|
expect(engine.sendIcmpEcho(byName('Notebook 1.11'), '141.99.2.10')).toMatchObject({ type: 'icmpError', kind: 'destinationNetworkUnreachable' });
|
|
expect(engine.sendIcmpEcho(byName('Notebook 1.11'), '141.99.1.10').type).toBe('delivered');
|
|
});
|
|
|
|
it('pings across three routers on manual routes and the routers\' default gateways', () => {
|
|
// Router 1 (141.99.1/2) sends everything else to 2.2; router 2 has manual
|
|
// routes to 1.0 and 5.0; router 3 (141.99.4/5) sends everything else to
|
|
// 4.1. The way back needs router 3's default gateway — which Swift drops.
|
|
const project = importFile(join(corpusRoot, 'java-examples/internetwork_komplett.fls'));
|
|
const notebook = project.graph.nodes.find((node) => node.displayName === 'Notebook 1.10')!.id;
|
|
const routers = project.graph.nodes.filter((node) => node.kind === 'router').map((node) => node.id);
|
|
|
|
expect(routers.map((router) => project.deviceConfigurations.get(router)?.defaultGateway)).toEqual([
|
|
'141.99.2.2',
|
|
'141.99.4.2',
|
|
'141.99.4.1',
|
|
]);
|
|
expect(startedEngine(project).sendIcmpEcho(notebook, '141.99.5.10').type).toBe('delivered');
|
|
|
|
const withoutRouterGateways = { ...project, deviceConfigurations: new Map(project.deviceConfigurations) };
|
|
for (const router of routers) withoutRouterGateways.deviceConfigurations.delete(router);
|
|
expect(startedEngine(withoutRouterGateways).sendIcmpEcho(notebook, '141.99.5.10').type).not.toBe('delivered');
|
|
});
|
|
});
|
|
|
|
// ── Rules the corpus does not exercise ─────────────────────────────────────
|
|
|
|
function flsNode(javaClass: FlsNode['javaClass'], fields: Partial<FlsNode> = {}): FlsNode {
|
|
return {
|
|
javaClass,
|
|
name: javaClass,
|
|
x: 0,
|
|
y: 0,
|
|
useIpAsName: false,
|
|
useMacAsName: false,
|
|
interfaces: [],
|
|
portCount: 0,
|
|
system: {},
|
|
source: { kind: 'object', className: javaClass, id: undefined, args: [], staticField: undefined,
|
|
factoryMethod: undefined, properties: new Map(), calls: [], presetElements: new Map(), presetEntries: new Map(), fromGetter: false },
|
|
...fields,
|
|
};
|
|
}
|
|
|
|
function topology(fields: Partial<FlsTopology>): FlsTopology {
|
|
return { versionLine: null, nodes: [], links: [], documentation: [], warnings: [], ...fields };
|
|
}
|
|
|
|
const nic = (ip: string | null, mask: string | null = '255.255.255.0') => ({ ip, mask, mac: undefined, gateway: '', dns: '' });
|
|
|
|
describe('importProject rules', () => {
|
|
it('labels a gateway wan0, lan0, gw2 and gives a rejected address the default', () => {
|
|
const project = importProject(
|
|
topology({ nodes: [flsNode('Gateway', { interfaces: [nic(null, null), nic('10.0.0.1'), nic('10.1.0.1')] })] }),
|
|
counter(),
|
|
);
|
|
const gateway = project.graph.nodes[0]!;
|
|
|
|
expect(gateway.ports.map((port) => port.label)).toEqual(['wan0', 'lan0', 'gw2']);
|
|
expect(gateway.ports.map((port) => project.interfaceConfigurations.get(interfaceKey(gateway.id, port.id)))).toEqual([
|
|
{ ipAddress: '42.0.0.10', subnetMask: '255.0.0.0' },
|
|
{ ipAddress: '10.0.0.1', subnetMask: '255.255.255.0' },
|
|
{ ipAddress: '10.1.0.1', subnetMask: '255.255.255.0' },
|
|
]);
|
|
expect(project.deviceConfigurations.get(gateway.id)).toEqual({
|
|
ipAddress: '42.0.0.10',
|
|
subnetMask: '255.0.0.0',
|
|
defaultGateway: '',
|
|
dnsServer: '',
|
|
});
|
|
});
|
|
|
|
it('keeps an empty name and shows the IP address only on hosts', () => {
|
|
const project = importProject(
|
|
topology({
|
|
nodes: [
|
|
flsNode('Vermittlungsrechner', { name: '', useIpAsName: true }),
|
|
flsNode('Notebook', { useIpAsName: true, useMacAsName: true }),
|
|
],
|
|
}),
|
|
counter(),
|
|
);
|
|
|
|
expect(project.graph.nodes.map((node) => [node.displayName, node.hostLabelPolicy])).toEqual([
|
|
['', 'manual'],
|
|
['Notebook', 'ipAndMAC'],
|
|
]);
|
|
});
|
|
|
|
it("takes a switch's and a router's port count from Java, and gives a rejected router address the default", () => {
|
|
const project = importProject(
|
|
topology({
|
|
nodes: [flsNode('Switch', { portCount: 3 }), flsNode('Vermittlungsrechner', { interfaces: [nic('10.0.0.1'), nic(null)] })],
|
|
}),
|
|
counter(),
|
|
);
|
|
const [networkSwitch, router] = project.graph.nodes;
|
|
|
|
expect(networkSwitch!.ports.map((port) => port.label)).toEqual(['sw1', 'sw2', 'sw3']);
|
|
expect(router!.ports.map((port) => project.interfaceConfigurations.get(interfaceKey(router!.id, port.id)))).toEqual([
|
|
{ ipAddress: '10.0.0.1', subnetMask: '255.255.255.0' },
|
|
{ ipAddress: '192.168.0.10', subnetMask: '255.255.255.0' },
|
|
]);
|
|
});
|
|
|
|
it('reads bold from the font style and alpha from the color', () => {
|
|
const project = importProject(
|
|
topology({
|
|
documentation: [
|
|
{ type: 2, text: 'b', x: 0, y: 0, width: 50, height: 30, color: [0, 0, 0, 51], font: { name: 'Dialog', size: 18, style: 1 } },
|
|
],
|
|
}),
|
|
counter(),
|
|
);
|
|
|
|
expect(project.documentationItems[0]).toMatchObject({ isBold: true, fontSize: 18, color: { alpha: 0.2 } });
|
|
});
|
|
|
|
it("maps a router's firewall, RIP and routes, and says what it cannot map", () => {
|
|
const project = importProject(
|
|
topology({
|
|
nodes: [
|
|
flsNode('Vermittlungsrechner', {
|
|
interfaces: [nic('10.0.0.1')],
|
|
system: {
|
|
ripEnabled: true,
|
|
manualRoutes: [
|
|
['10.1.0.0', '255.255.0.0', '10.0.0.2', '10.0.0.1'],
|
|
['10.2.0.0', '255.255.0.0', null, '10.0.0.1'],
|
|
],
|
|
firewall: {
|
|
activated: true,
|
|
defaultPolicy: 1,
|
|
dropICMP: true,
|
|
filterSYNSegmentsOnly: false,
|
|
filterUdp: false,
|
|
rules: [
|
|
{ srcIP: '10.0.0.0', srcMask: '255.0.0.0', destIP: null, destMask: '', port: 80, protocol: -1, action: 1 },
|
|
{ srcIP: '', srcMask: '', destIP: '', destMask: '', port: -1, protocol: 99, action: 0 },
|
|
],
|
|
},
|
|
},
|
|
}),
|
|
],
|
|
}),
|
|
counter(),
|
|
);
|
|
const router = project.graph.nodes[0]!.id;
|
|
|
|
expect(project.ripEnabledByNodeId.get(router)).toBe(true);
|
|
expect(project.manualRoutesByNodeId.get(router)).toEqual([
|
|
{ destinationNetwork: '10.1.0.0', subnetMask: '255.255.0.0', gateway: '10.0.0.2', interfaceIPAddress: '10.0.0.1' },
|
|
]);
|
|
expect(project.firewallConfigurationsByNodeId.get(router)).toEqual({
|
|
isActive: true,
|
|
defaultPolicy: 'accept',
|
|
dropIcmp: true,
|
|
filterSynSegmentsOnly: false,
|
|
filterUdp: false,
|
|
rules: [
|
|
{
|
|
sourceIPAddress: '10.0.0.0',
|
|
sourceSubnetMask: '255.0.0.0',
|
|
destinationIPAddress: '',
|
|
destinationSubnetMask: '',
|
|
port: 80,
|
|
protocol: 'all',
|
|
action: 'accept',
|
|
},
|
|
{
|
|
sourceIPAddress: '',
|
|
sourceSubnetMask: '',
|
|
destinationIPAddress: '',
|
|
destinationSubnetMask: '',
|
|
port: -1,
|
|
protocol: 'tcp',
|
|
action: 'drop',
|
|
},
|
|
],
|
|
});
|
|
expect(project.warnings).toEqual([
|
|
"node 'Vermittlungsrechner': a manual route without four entries, left out",
|
|
"node 'Vermittlungsrechner': firewall protocol 99 unknown, used TCP",
|
|
]);
|
|
});
|
|
|
|
it("gives a switch without SSID one from its id, and joins a host to a wireless network only with an SSID", () => {
|
|
const project = importProject(
|
|
topology({
|
|
nodes: [
|
|
flsNode('Switch', { portCount: 24, system: { ssid: undefined, retentionTime: 2000 } }),
|
|
flsNode('Switch', { portCount: 24, system: { ssid: ' MyWiFi ', retentionTime: 300_000 } }),
|
|
flsNode('Notebook', { interfaces: [nic('10.0.0.1')], system: { ssid: 'MyWiFi', manualRoutes: [] } }),
|
|
flsNode('Rechner', { interfaces: [nic('10.0.0.2')], system: { ssid: ' ', manualRoutes: [] } }),
|
|
],
|
|
}),
|
|
counter(),
|
|
);
|
|
const [first, second, notebook, pc] = project.graph.nodes.map((node) => node.id);
|
|
|
|
expect(project.switchConfigurations.get(first!)).toEqual({ ssid: defaultSwitchConfiguration(first!).ssid, retentionTimeMilliseconds: 2000 });
|
|
expect(project.switchConfigurations.get(second!)).toEqual({ ssid: 'MyWiFi', retentionTimeMilliseconds: 300_000 });
|
|
expect(project.hostWirelessConfigurations.get(notebook!)).toEqual({ isEnabled: true, ssid: 'MyWiFi' });
|
|
expect(project.hostWirelessConfigurations.has(pc!)).toBe(false);
|
|
});
|
|
|
|
it('leaves out a cable to itself and one to a port that already has a cable', () => {
|
|
const project = importProject(
|
|
topology({
|
|
nodes: [flsNode('Switch', { portCount: 24 }), flsNode('Rechner', { interfaces: [nic('10.0.0.1')] })],
|
|
links: [
|
|
{ ends: [{ node: 0, port: 0 }, { node: 0, port: 1 }], wireless: false },
|
|
{ ends: [{ node: 1, port: 0 }, { node: 0, port: 2 }], wireless: false },
|
|
{ ends: [{ node: 0, port: 3 }, { node: 1, port: 0 }], wireless: false },
|
|
],
|
|
}),
|
|
counter(),
|
|
);
|
|
|
|
expect(project.graph.links).toHaveLength(1);
|
|
expect(project.warnings).toEqual([
|
|
'cable 1: connects a node to itself, left out',
|
|
'cable 3: a port already has a cable, left out',
|
|
]);
|
|
});
|
|
|
|
it('leaves out documentation items without size or of an unknown type, and colors the one without color', () => {
|
|
const item = { text: 'Hallo', x: 10, y: 20, width: 100, height: 30, font: null };
|
|
const project = importProject(
|
|
topology({
|
|
documentation: [
|
|
{ ...item, type: 2, color: null },
|
|
{ ...item, type: 1, width: 0, color: [1, 2, 3, 255] },
|
|
{ ...item, type: 3, color: [1, 2, 3, 255] },
|
|
],
|
|
}),
|
|
counter(),
|
|
);
|
|
|
|
expect(project.documentationItems.map((doc) => [doc.kind, doc.text, doc.color, doc.fontName, doc.fontSize])).toEqual([
|
|
['text', 'Hallo', { red: 0, green: 0, blue: 0, alpha: 1 }, 'Dialog', 12],
|
|
]);
|
|
expect(project.warnings).toEqual([
|
|
'documentation item 1 without a color, given the default',
|
|
'documentation item without size, left out',
|
|
'documentation item of type 3, left out (known: RECT = 1, TEXT = 2)',
|
|
]);
|
|
});
|
|
});
|