feat(editor): F4.5e a router's connections in the device sheet
SheetRun now clicks through Java's JConnectionsDialog with a router, a computer and a switch cabled in the GUIContainer: a click marks a card, the same click unmarks it, another card swaps the cables or moves the one; "+" adds 192.168.0.10/24 up to eight cards, "-" removes the last card with its cable down to none, and a router without a card keeps no gateway. The document adds, removes and swaps a router's cards (the same cables, the same devices), Java's click logic is one function for the test and the page, and the sheet names the address at a cable's far end. The page "Verbindungen verwalten" shows Java's RJ45 buttons in Java's colours with each card's far end below. Not adopted: the last card stays. A card tab or a mark whose card is undone away falls back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -33,8 +33,12 @@ import { emptyProject, inOrder, nodeEntry, ROOTS } from './projectDoc.js';
|
||||
/** The kinds of the palette, in Java's order (GUIDesignSidebar.addComponentItemsToSidebar; the cable comes first, a tool). */
|
||||
export const PALETTE: readonly Exclude<NodeKind, 'unsupported'>[] = ['pc', 'notebook', 'networkSwitch', 'gateway', 'router', 'remoteLink'];
|
||||
|
||||
/** A router's interfaces, as Java's dialog offers them (2 to 8, 2 when it is closed). */
|
||||
export const ROUTER_INTERFACES = { minimum: 2, maximum: 8, standard: 2 } as const;
|
||||
/**
|
||||
* A router's interfaces, as Java's dialog offers them (2 to 8, 2 when it is
|
||||
* closed); its connections dialog removes them down to `fewest` here (Java:
|
||||
* none — see settings.ts).
|
||||
*/
|
||||
export const ROUTER_INTERFACES = { minimum: 2, maximum: 8, standard: 2, fewest: 1 } as const;
|
||||
|
||||
export interface NewDevice {
|
||||
readonly kind: Exclude<NodeKind, 'unsupported'>;
|
||||
|
||||
@@ -27,7 +27,10 @@
|
||||
// taken, and the sheet says why. Nor that Java's firewall table writes a
|
||||
// cell into the rule before it checks it, so that a rule holds "abc" or a
|
||||
// mask of 255.0.255.0 while the table shows the old value, and reads a port
|
||||
// that is no number as "all ports".
|
||||
// that is no number as "all ports". Nor that Java's connections dialog
|
||||
// removes a router's last card too: a router without one keeps no gateway
|
||||
// (setStandardGateway writes it into the cards; SheetRun's "connections"),
|
||||
// nor could a file hold it; here one card stays.
|
||||
|
||||
import * as Y from 'yjs';
|
||||
import {
|
||||
@@ -47,7 +50,7 @@ import {
|
||||
type NodeKind,
|
||||
} from '@mdg-filius/core';
|
||||
import { modemPair } from '@mdg-filius/fls';
|
||||
import { connect, type ConnectError } from './commands.js';
|
||||
import { connect, ROUTER_INTERFACES, type ConnectError } from './commands.js';
|
||||
import { checkField, type CheckedField, type FieldRule } from './fields.js';
|
||||
import { inOrder, ROOTS } from './projectDoc.js';
|
||||
import { fromY, toY, type PlainValue } from './values.js';
|
||||
@@ -295,6 +298,87 @@ export function setRip(doc: Y.Doc, id: Id, on: boolean, origin: unknown): void {
|
||||
}, origin);
|
||||
}
|
||||
|
||||
/** A router's ports, or none for another device. */
|
||||
function routerPorts(doc: Y.Doc, id: Id): Y.Array<Y.Map<unknown>> | undefined {
|
||||
const node = nodes(doc).get(id);
|
||||
return node === undefined || kindOf(node) !== 'router' ? undefined : portsOf(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* A card added to a router after its last (JConnectionsDialog's "+":
|
||||
* hinzuAnschluss, a new NetzwerkInterface with 192.168.0.10/24 and its own
|
||||
* MAC address, here from the port's id), up to eight; says whether it was.
|
||||
*/
|
||||
export function addRouterInterface(doc: Y.Doc, id: Id, portId: Id, origin: unknown): boolean {
|
||||
const ports = routerPorts(doc, id);
|
||||
if (ports === undefined || ports.length >= ROUTER_INTERFACES.maximum) return false;
|
||||
doc.transact(() => {
|
||||
const port = new Y.Map<unknown>();
|
||||
port.set('id', portId);
|
||||
port.set('label', `rt${ports.length + 1}`);
|
||||
port.set('occupied', false);
|
||||
port.set('config', toY({ ipAddress: '192.168.0.10', subnetMask: '255.255.255.0' }));
|
||||
ports.push([port]);
|
||||
}, origin);
|
||||
return true;
|
||||
}
|
||||
|
||||
type PluggedEnd = { readonly linkId: Id; readonly link: Y.Map<unknown>; readonly end: 'sourcePort' | 'targetPort' };
|
||||
|
||||
/** The cables at a device's port, with the end that plugs into it. */
|
||||
function cablesAt(doc: Y.Doc, id: Id, portId: Id): PluggedEnd[] {
|
||||
const ends: PluggedEnd[] = [];
|
||||
for (const [linkId, link] of links(doc).entries()) {
|
||||
if (link.get('sourceNode') === id && link.get('sourcePort') === portId) ends.push({ linkId, link, end: 'sourcePort' });
|
||||
else if (link.get('targetNode') === id && link.get('targetPort') === portId) ends.push({ linkId, link, end: 'targetPort' });
|
||||
}
|
||||
return ends;
|
||||
}
|
||||
|
||||
/** A router's last card removed with its cable (JConnectionsDialog's "-"), down to one; says whether it was. */
|
||||
export function removeRouterInterface(doc: Y.Doc, id: Id, origin: unknown): boolean {
|
||||
const ports = routerPorts(doc, id);
|
||||
if (ports === undefined || ports.length <= ROUTER_INTERFACES.fewest) return false;
|
||||
const last = ports.get(ports.length - 1).get('id') as Id;
|
||||
doc.transact(() => {
|
||||
for (const { linkId } of cablesAt(doc, id, last)) links(doc).delete(linkId);
|
||||
ports.delete(ports.length - 1);
|
||||
}, origin);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Two of a router's cards trade their cables (JConnectionsDialog.swapConnection):
|
||||
* both plugged, the cables swap; one plugged, its cable moves to the other
|
||||
* card; neither, nothing changes. The cables stay the same cables, joining the
|
||||
* same devices. Says whether a cable moved.
|
||||
*/
|
||||
export function swapRouterCables(doc: Y.Doc, id: Id, first: Id, second: Id, origin: unknown): boolean {
|
||||
const ports = routerPorts(doc, id);
|
||||
const has = (portId: Id) => ports?.toArray().some((port) => port.get('id') === portId) === true;
|
||||
if (first === second || !has(first) || !has(second)) return false;
|
||||
const atFirst = cablesAt(doc, id, first);
|
||||
const atSecond = cablesAt(doc, id, second);
|
||||
if (atFirst.length === 0 && atSecond.length === 0) return false;
|
||||
doc.transact(() => {
|
||||
for (const { link, end } of atFirst) link.set(end, second);
|
||||
for (const { link, end } of atSecond) link.set(end, first);
|
||||
}, origin);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* A click on a card's button in Java's connections dialog, with the card
|
||||
* marked before (null for none): the first click marks a card, a click on it
|
||||
* again unmarks it, a click on another card swaps the two cards' cables
|
||||
* (swapRouterCables) and leaves none marked.
|
||||
*/
|
||||
export function connectionsClick(marked: number | null, clicked: number): { readonly marked: number | null; readonly swap?: readonly [number, number] } {
|
||||
if (marked === null) return { marked: clicked };
|
||||
if (marked === clicked) return { marked: null };
|
||||
return { marked: null, swap: [marked, clicked] };
|
||||
}
|
||||
|
||||
/** A column of the forwarding table's manual rows. */
|
||||
export type RouteColumn = keyof ManualRoute;
|
||||
|
||||
|
||||
@@ -88,8 +88,13 @@ export interface InterfaceSheet {
|
||||
readonly ipAddress: string;
|
||||
readonly subnetMask: string;
|
||||
readonly macAddress: string;
|
||||
/** The device at the other end of its cable (its label, as Java names it) and the cable; null without one. */
|
||||
readonly connected: { readonly device: string; readonly link: string } | null;
|
||||
/**
|
||||
* The device at the other end of its cable (its label, as Java names it),
|
||||
* the address of the card the cable plugs into there (a computer's, a
|
||||
* router's; null for a switch or a modem, which have none) and the cable;
|
||||
* null without one.
|
||||
*/
|
||||
readonly connected: { readonly device: string; readonly address: string | null; readonly link: string } | null;
|
||||
}
|
||||
|
||||
/** A router's or home router's firewall as Java's dialog shows it (JFirewallDialog). */
|
||||
@@ -142,6 +147,22 @@ const setting = (node: Y.Map<unknown>, key: string) => settingsOf(node).get(key)
|
||||
|
||||
const text = (map: Y.Map<unknown> | undefined, key: string) => (map?.get(key) as string | undefined) ?? '';
|
||||
|
||||
/** The address of a device's card at a port, as Java's NetzwerkInterface holds it; null for a device without cards (InternetKnoten). */
|
||||
function cardAddress(node: Y.Map<unknown>, portId: string): string | null {
|
||||
switch (node.get('kind') as NodeKind) {
|
||||
case 'pc':
|
||||
case 'notebook':
|
||||
return text(setting(node, 'device'), 'ipAddress');
|
||||
case 'router':
|
||||
case 'gateway': {
|
||||
const port = (node.get('ports') as Y.Array<Y.Map<unknown>>).toArray().find((each) => each.get('id') === portId);
|
||||
return text(port?.get('config') as Y.Map<unknown> | undefined, 'ipAddress');
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** A router's or home router's cards, each with what its cable joins. */
|
||||
function interfacesOf(doc: Y.Doc, id: string, node: Y.Map<unknown>): InterfaceSheet[] {
|
||||
const all = doc.getMap<Y.Map<unknown>>(ROOTS.nodes);
|
||||
@@ -150,14 +171,14 @@ function interfacesOf(doc: Y.Doc, id: string, node: Y.Map<unknown>): InterfaceSh
|
||||
const portId = port.get('id') as string;
|
||||
const config = port.get('config') as Y.Map<unknown> | undefined;
|
||||
const cable = links.find(([, link]) => (link.get('sourceNode') === id && link.get('sourcePort') === portId) || (link.get('targetNode') === id && link.get('targetPort') === portId));
|
||||
const other =
|
||||
cable === undefined ? undefined : all.get((cable[1].get('sourceNode') === id && cable[1].get('sourcePort') === portId ? cable[1].get('targetNode') : cable[1].get('sourceNode')) as string);
|
||||
const near = cable !== undefined && cable[1].get('sourceNode') === id && cable[1].get('sourcePort') === portId;
|
||||
const other = cable === undefined ? undefined : all.get(cable[1].get(near ? 'targetNode' : 'sourceNode') as string);
|
||||
return {
|
||||
portId,
|
||||
ipAddress: text(config, 'ipAddress'),
|
||||
subnetMask: text(config, 'subnetMask'),
|
||||
macAddress: (port.get('mac') as string | undefined) ?? stableGeneratedMacAddress(portId),
|
||||
connected: cable === undefined || other === undefined ? null : { device: nodeLabel(other), link: cable[0] },
|
||||
connected: cable === undefined || other === undefined ? null : { device: nodeLabel(other), address: cardAddress(other, cable[1].get(near ? 'targetPort' : 'sourcePort') as string), link: cable[0] },
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -31,6 +31,10 @@ import {
|
||||
setSwitchRetention,
|
||||
setSwitchSsid,
|
||||
addRoute,
|
||||
addRouterInterface,
|
||||
connectionsClick,
|
||||
removeRouterInterface,
|
||||
swapRouterCables,
|
||||
addFirewallRule,
|
||||
moveFirewallRule,
|
||||
removeFirewallRule,
|
||||
@@ -104,8 +108,28 @@ const java = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)
|
||||
readonly cells: readonly { readonly column: number; readonly input: string; readonly rule: JavaFirewallRule; readonly valid: boolean }[];
|
||||
readonly moves: readonly (readonly [string, number, boolean | null, readonly string[]])[];
|
||||
};
|
||||
readonly connections: {
|
||||
readonly steps: readonly JavaConnectionsStep[];
|
||||
readonly hostPortFree: boolean;
|
||||
readonly switchPortsFree: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
/** A click in Java's connections dialog ("c<i>" a card's button, "+", "-") and what the dialog and the router show then. */
|
||||
interface JavaConnectionsStep {
|
||||
readonly click: string;
|
||||
readonly local: readonly string[];
|
||||
readonly remote: readonly string[];
|
||||
readonly buttons: readonly ('off' | 'marked' | 'plugged' | 'free')[];
|
||||
readonly add: boolean;
|
||||
readonly remove: boolean;
|
||||
/** Address, mask and the device its cable joins ("" for none), per card. */
|
||||
readonly cards: readonly (readonly [string, string, string])[];
|
||||
readonly cables: number;
|
||||
readonly panelError?: string | null;
|
||||
readonly gateway?: string | null;
|
||||
}
|
||||
|
||||
interface JavaFirewallRule {
|
||||
readonly srcIP: string;
|
||||
readonly srcMask: string;
|
||||
@@ -654,7 +678,7 @@ describe("a router's sheet", () => {
|
||||
const pc = add(doc, 'pc', 'PC-A');
|
||||
setHostLabels(doc, pc, { ipAddress: true, macAddress: false }, 'me');
|
||||
connect(doc, { node: pc }, { node: id }, 'cable', 'me');
|
||||
expect(router(doc, id).interfaces.map((card) => card.connected)).toEqual([{ device: '192.168.0.10', link: 'cable' }, null]);
|
||||
expect(router(doc, id).interfaces.map((card) => card.connected)).toEqual([{ device: '192.168.0.10', address: '192.168.0.10', link: 'cable' }, null]);
|
||||
expect(router(doc, id).interfaces[0]!.macAddress).toMatch(/^([0-9A-F]{2}:){5}[0-9A-F]{2}$/);
|
||||
});
|
||||
});
|
||||
@@ -1089,3 +1113,196 @@ describe("a router's firewall", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("a router's connections", () => {
|
||||
/** SheetRun's network: router R with three cards, PC A plugged into the first, switch S into the second. */
|
||||
function network() {
|
||||
const doc = new Y.Doc();
|
||||
const id = add(doc, 'router', 'R', 3);
|
||||
router(doc, id).interfaces.forEach((card, index) => setInterfaceAddress(doc, id, card.portId, 'ipAddress', `10.${index}.0.1`, 'me'));
|
||||
const pc = add(doc, 'pc', 'A');
|
||||
setHostAddress(doc, pc, 'ipAddress', '10.0.0.2', 'me');
|
||||
const sw = add(doc, 'networkSwitch', 'S');
|
||||
expect(connect(doc, { node: pc }, { node: id }, 'cable-a', 'me')).toEqual({ link: 'cable-a' });
|
||||
expect(connect(doc, { node: id, port: router(doc, id).interfaces[1]!.portId }, { node: sw }, 'cable-s', 'me')).toEqual({ link: 'cable-s' });
|
||||
return { doc, id, pc, sw };
|
||||
}
|
||||
|
||||
/** What Java's dialog would show of the router, with the card marked. */
|
||||
function shown(doc: Y.Doc, id: string, marked: number | null) {
|
||||
const cards = router(doc, id).interfaces;
|
||||
const pad = <T,>(each: T[], blank: T) => [...each, ...Array<T>(8 - each.length).fill(blank)];
|
||||
return {
|
||||
local: pad(cards.map((card, index) => `NIC ${index + 1}: ${card.ipAddress}`), ''),
|
||||
remote: pad(cards.map((card) => (card.connected === null ? '' : card.connected.address === null ? card.connected.device : `${card.connected.device} (${card.connected.address})`)), ''),
|
||||
buttons: pad(cards.map((card, index) => (index === marked ? 'marked' : card.connected === null ? 'free' : 'plugged')), 'off'),
|
||||
add: cards.length < 8,
|
||||
remove: cards.length > 1,
|
||||
cards: cards.map((card) => [card.ipAddress, card.subnetMask, card.connected?.device ?? '']),
|
||||
cables: doc.getMap(ROOTS.links).size,
|
||||
};
|
||||
}
|
||||
|
||||
it('swap, add and remove cards as Java’s dialog does, click by click, down to the last card', () => {
|
||||
const { doc, id } = network();
|
||||
let marked: number | null = null;
|
||||
const steps = java.connections.steps;
|
||||
const last = steps.findIndex((step) => step.cards.length === 1);
|
||||
for (const [index, step] of steps.slice(0, last + 1).entries()) {
|
||||
if (step.click.startsWith('c')) {
|
||||
const next = connectionsClick(marked, Number(step.click.slice(1)));
|
||||
if (next.swap !== undefined) {
|
||||
const ports = router(doc, id).interfaces.map((card) => card.portId);
|
||||
swapRouterCables(doc, id, ports[next.swap[0]]!, ports[next.swap[1]]!, 'me');
|
||||
}
|
||||
marked = next.marked;
|
||||
} else if (step.click === '+') {
|
||||
// Java's "+" is greyed out at eight cards; its click does nothing then.
|
||||
expect(addRouterInterface(doc, id, newId(), 'me'), `step ${index}`).toBe(steps[index - 1]!.add);
|
||||
marked = null;
|
||||
} else if (step.click === '-') {
|
||||
expect(removeRouterInterface(doc, id, 'me'), `step ${index}`).toBe(true);
|
||||
marked = null;
|
||||
}
|
||||
const { local, remote, buttons, add: addable, remove, cards, cables } = step;
|
||||
// Java's "-" stays open at one card; here the last card stays (below).
|
||||
expect(shown(doc, id, marked), `step ${index} ${step.click}`).toEqual({ local, remote, buttons, add: addable, remove: index === last ? false : remove, cards, cables });
|
||||
}
|
||||
expect(steps[last]!.remove).toBe(true);
|
||||
expect(steps.at(-1)!.cards).toEqual([]);
|
||||
// Java's router without a card keeps no gateway; the one with a card does.
|
||||
expect([steps[last]!.gateway, steps.at(-1)!.gateway]).toEqual(['10.0.0.254', null]);
|
||||
expect(steps[last]!.panelError).toBeNull();
|
||||
});
|
||||
|
||||
it('keep the last card, with its cable, and its gateway', () => {
|
||||
const doc = new Y.Doc();
|
||||
const id = add(doc, 'router', 'R', 2);
|
||||
const pc = add(doc, 'pc', 'A');
|
||||
connect(doc, { node: pc }, { node: id }, 'cable', 'me');
|
||||
setRouterGateway(doc, id, '192.168.0.254', 'me');
|
||||
expect(removeRouterInterface(doc, id, 'me')).toBe(true);
|
||||
expect(removeRouterInterface(doc, id, 'me')).toBe(false);
|
||||
expect(router(doc, id).interfaces).toHaveLength(1);
|
||||
expect(router(doc, id).interfaces[0]!.connected).toMatchObject({ device: 'A', link: 'cable' });
|
||||
expect(router(doc, id).gateway).toBe('192.168.0.254');
|
||||
});
|
||||
|
||||
it('remove the last card’s cable with it, and free the other device’s port', () => {
|
||||
const { doc, id, sw } = network();
|
||||
const ports = router(doc, id).interfaces.map((card) => card.portId);
|
||||
expect(swapRouterCables(doc, id, ports[1]!, ports[2]!, 'me')).toBe(true);
|
||||
expect(removeRouterInterface(doc, id, 'me')).toBe(true);
|
||||
expect([...doc.getMap<Y.Map<unknown>>(ROOTS.links).keys()]).toEqual(['cable-a']);
|
||||
expect(java.connections.switchPortsFree).toBe(true);
|
||||
// The switch's port takes another cable.
|
||||
const pc = add(doc, 'pc', 'B');
|
||||
expect(connect(doc, { node: pc }, { node: sw }, 'cable-b', 'me')).toEqual({ link: 'cable-b' });
|
||||
expect(java.connections.hostPortFree).toBe(true);
|
||||
});
|
||||
|
||||
it('give a new card Java’s address and mask, its own MAC address, the next label and no cable', () => {
|
||||
const { doc, id } = network();
|
||||
expect(addRouterInterface(doc, id, 'port-4', 'me')).toBe(true);
|
||||
const cards = router(doc, id).interfaces;
|
||||
expect(cards[3]).toMatchObject({ portId: 'port-4', ipAddress: '192.168.0.10', subnetMask: '255.255.255.0', connected: null });
|
||||
expect(new Set(cards.map((card) => card.macAddress)).size).toBe(4);
|
||||
const ports = (doc.getMap<Y.Map<unknown>>(ROOTS.nodes).get(id)!.get('ports') as Y.Array<Y.Map<unknown>>).toArray();
|
||||
expect(ports.map((port) => port.get('label'))).toEqual(['rt1', 'rt2', 'rt3', 'rt4']);
|
||||
expect(ports[3]!.get('occupied')).toBe(false);
|
||||
});
|
||||
|
||||
it('keep each cable’s identity and devices when cards swap, and move nothing between free cards', () => {
|
||||
const { doc, id, pc, sw } = network();
|
||||
const ports = router(doc, id).interfaces.map((card) => card.portId);
|
||||
const ends = () =>
|
||||
[...doc.getMap<Y.Map<unknown>>(ROOTS.links).entries()].map(([linkId, link]) => [linkId, link.get('sourceNode'), link.get('sourcePort'), link.get('targetNode'), link.get('targetPort')]);
|
||||
const before = ends();
|
||||
expect(swapRouterCables(doc, id, ports[0]!, ports[1]!, 'me')).toBe(true);
|
||||
// The PC plugged first (the router is the cable's target), the router into the switch (its source).
|
||||
expect(ends()).toEqual([
|
||||
['cable-a', pc, before[0]![2], id, ports[1]],
|
||||
['cable-s', id, ports[0], sw, before[1]![4]],
|
||||
]);
|
||||
addRouterInterface(doc, id, 'port-4', 'me');
|
||||
const now = ends();
|
||||
expect(swapRouterCables(doc, id, ports[2]!, 'port-4', 'me')).toBe(false);
|
||||
expect(ends()).toEqual(now);
|
||||
});
|
||||
|
||||
it('refuse a card twice, a card not the router’s, and devices that are no router', () => {
|
||||
const { doc, id, pc } = network();
|
||||
const ports = router(doc, id).interfaces.map((card) => card.portId);
|
||||
const updates: unknown[] = [];
|
||||
doc.on('update', (update) => updates.push(update));
|
||||
expect(swapRouterCables(doc, id, ports[0]!, ports[0]!, 'me')).toBe(false);
|
||||
expect(swapRouterCables(doc, id, ports[0]!, 'no-port', 'me')).toBe(false);
|
||||
expect(swapRouterCables(doc, id, 'no-port', ports[0]!, 'me')).toBe(false);
|
||||
const pcPort = (doc.getMap<Y.Map<unknown>>(ROOTS.nodes).get(pc)!.get('ports') as Y.Array<Y.Map<unknown>>).get(0).get('id') as string;
|
||||
expect(swapRouterCables(doc, id, ports[2]!, pcPort, 'me')).toBe(false);
|
||||
expect(swapRouterCables(doc, 'nobody', ports[0]!, ports[1]!, 'me')).toBe(false);
|
||||
const homeId = add(doc, 'gateway');
|
||||
const [wan, lan] = [home(doc, homeId).wan.portId, home(doc, homeId).lan.portId];
|
||||
expect(swapRouterCables(doc, homeId, wan, lan, 'me')).toBe(false);
|
||||
expect(addRouterInterface(doc, homeId, 'port-x', 'me')).toBe(false);
|
||||
expect(removeRouterInterface(doc, homeId, 'me')).toBe(false);
|
||||
expect(addRouterInterface(doc, pc, 'port-x', 'me')).toBe(false);
|
||||
expect(removeRouterInterface(doc, 'nobody', 'me')).toBe(false);
|
||||
const before = updates.length;
|
||||
expect(before).toBeGreaterThan(0);
|
||||
for (let more = 3; more < 8; more++) expect(addRouterInterface(doc, id, `port-${more}`, 'me')).toBe(true);
|
||||
expect(addRouterInterface(doc, id, 'port-9', 'me')).toBe(false);
|
||||
expect(router(doc, id).interfaces).toHaveLength(8);
|
||||
expect(updates).toHaveLength(before + 5);
|
||||
});
|
||||
|
||||
it('click as Java’s dialog does: mark, unmark, swap', () => {
|
||||
expect(connectionsClick(null, 2)).toEqual({ marked: 2 });
|
||||
expect(connectionsClick(2, 2)).toEqual({ marked: null });
|
||||
expect(connectionsClick(2, 0)).toEqual({ marked: null, swap: [2, 0] });
|
||||
});
|
||||
|
||||
it('are each one step of the person’s undo, and go into the file', () => {
|
||||
const { doc, id } = network();
|
||||
const undo = createUndoManager(doc, 'me');
|
||||
const ports = router(doc, id).interfaces.map((card) => card.portId);
|
||||
undo.stopCapturing();
|
||||
addRouterInterface(doc, id, 'port-4', 'me');
|
||||
undo.stopCapturing();
|
||||
swapRouterCables(doc, id, ports[0]!, 'port-4', 'me');
|
||||
undo.stopCapturing();
|
||||
removeRouterInterface(doc, id, 'me');
|
||||
expect(router(doc, id).interfaces.map((card) => card.connected?.device ?? '')).toEqual(['', 'S', '']);
|
||||
undo.undo();
|
||||
expect(router(doc, id).interfaces.map((card) => card.connected?.device ?? '')).toEqual(['', 'S', '', 'A']);
|
||||
undo.undo();
|
||||
expect(router(doc, id).interfaces.map((card) => card.connected?.device ?? '')).toEqual(['A', 'S', '', '']);
|
||||
swapRouterCables(doc, id, ports[0]!, 'port-4', 'me');
|
||||
let counter = 0;
|
||||
const reopened = openProjectArchive(saveProjectArchive(readProject(doc)).archive, () => `70000000-0000-0000-0000-${(++counter).toString(16).padStart(12, '0')}`);
|
||||
const again = reopened.graph.nodes.find((node) => node.displayName === 'R')!;
|
||||
expect(again.ports.map((port) => port.label)).toEqual(['rt1', 'rt2', 'rt3', 'rt4']);
|
||||
const plugged = reopened.graph.links.flatMap((link) => [link.sourcePortId, link.targetPortId]);
|
||||
expect(again.ports.map((port) => plugged.includes(port.id))).toEqual([false, true, false, true]);
|
||||
});
|
||||
|
||||
it('name the address of the card a cable plugs into: a computer’s, a router’s, none for a switch', () => {
|
||||
const { doc, id } = network();
|
||||
const other = add(doc, 'router', 'R2');
|
||||
setInterfaceAddress(doc, other, router(doc, other).interfaces[1]!.portId, 'ipAddress', '10.2.0.2', 'me');
|
||||
connect(doc, { node: id, port: router(doc, id).interfaces[2]!.portId }, { node: other, port: router(doc, other).interfaces[1]!.portId }, 'cable-r', 'me');
|
||||
const homeId = add(doc, 'gateway', 'H');
|
||||
const modem = add(doc, 'remoteLink', 'M');
|
||||
addRouterInterface(doc, id, 'port-4', 'me');
|
||||
addRouterInterface(doc, id, 'port-5', 'me');
|
||||
connect(doc, { node: homeId, port: home(doc, homeId).lan.portId }, { node: id, port: 'port-4' }, 'cable-h', 'me');
|
||||
connect(doc, { node: modem }, { node: id, port: 'port-5' }, 'cable-m', 'me');
|
||||
expect(router(doc, id).interfaces.map((card) => card.connected && [card.connected.device, card.connected.address])).toEqual([
|
||||
['A', '10.0.0.2'],
|
||||
['S', null],
|
||||
['R2', '10.2.0.2'],
|
||||
['H', '192.168.0.10'],
|
||||
['M', null],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user