tscircuit/ti

ti tscircuit library

Version
1.0.192
License
unset
Stars
0

lib/subcircuits/PinchDetection_INA240_TLV2316_LMV7275.test.tsx

import { beforeAll, expect, test } from "bun:test";
import { Circuit } from "@tscircuit/core";
import type { AnyCircuitElement } from "circuit-json";
import {
  TIDA01421_ALTIUM_SCALE,
  tida01421Position,
} from "../tida01421-coordinates.ts";
import {
  PinchDetectionPower_TPS7B69,
  TIDA01421_POWER_ORIGIN,
} from "./PinchDetectionPower_TPS7B69.circuit.tsx";
import {
  PinchDetectionSignalChain_INA240_TLV2316_LMV7275,
  TIDA01421_SIGNAL_CHAIN_ORIGIN,
} from "./PinchDetectionSignalChain_INA240_TLV2316_LMV7275.circuit.tsx";
import { PinchDetection_INA240_TLV2316_LMV7275 } from "./PinchDetection_INA240_TLV2316_LMV7275.circuit.tsx";

type SourceComponent = Extract<AnyCircuitElement, { type: "source_component" }>;
type SourceNet = Extract<AnyCircuitElement, { type: "source_net" }>;
type SourcePort = Extract<AnyCircuitElement, { type: "source_port" }>;
type SourceTrace = Extract<AnyCircuitElement, { type: "source_trace" }>;
type SchematicText = Extract<AnyCircuitElement, { type: "schematic_text" }>;
type SchematicComponent = Extract<
  AnyCircuitElement,
  { type: "schematic_component" }
>;
type SchematicPort = Extract<AnyCircuitElement, { type: "schematic_port" }>;
type SchematicNetLabel = Extract<
  AnyCircuitElement,
  { type: "schematic_net_label" }
>;
type SchematicTrace = Extract<AnyCircuitElement, { type: "schematic_trace" }>;

let signalJson: AnyCircuitElement[];
let powerJson: AnyCircuitElement[];
let compositeJson: AnyCircuitElement[];

const renderSchematic = async (element: React.ReactElement) => {
  const circuit = new Circuit({
    platform: { pcbDisabled: true, partsEngineDisabled: true },
  });
  circuit.add(element);
  await circuit.renderUntilSettled();
  return circuit.getCircuitJson();
};

beforeAll(async () => {
  const originalConsoleError = console.error;
  console.error = (...args: unknown[]) => {
    const isExpectedSchematicOnlyFootprintMessage = args.some((arg) =>
      String(arg).includes(
        'No footprint resolver is configured for library "kicad"',
      ),
    );
    if (!isExpectedSchematicOnlyFootprintMessage) originalConsoleError(...args);
  };
  try {
    [signalJson, powerJson, compositeJson] = await Promise.all([
      renderSchematic(
        <PinchDetectionSignalChain_INA240_TLV2316_LMV7275 name="pinch" />,
      ),
      renderSchematic(<PinchDetectionPower_TPS7B69 name="power" />),
      renderSchematic(
        <PinchDetection_INA240_TLV2316_LMV7275 name="pinchComposite" />,
      ),
    ]);
  } finally {
    console.error = originalConsoleError;
  }
}, 30_000);

const getComponent = (circuitJson: AnyCircuitElement[], name: string) => {
  const component = circuitJson.find(
    (element): element is SourceComponent =>
      element.type === "source_component" && element.name === name,
  );
  if (!component) throw new Error(`Missing source component ${name}`);
  return component;
};

const getConnectivityKey = (
  circuitJson: AnyCircuitElement[],
  componentName: string,
  pinNumber: number,
) => {
  const port = getSourcePort(circuitJson, componentName, pinNumber);
  if (!port.subcircuit_connectivity_map_key) {
    throw new Error(
      `Missing connectivity key for ${componentName}.${pinNumber}`,
    );
  }
  return port.subcircuit_connectivity_map_key;
};

const getSourcePort = (
  circuitJson: AnyCircuitElement[],
  componentName: string,
  pinNumber: number,
) => {
  const component = getComponent(circuitJson, componentName);
  const port = circuitJson.find(
    (element): element is SourcePort =>
      element.type === "source_port" &&
      element.source_component_id === component.source_component_id &&
      element.pin_number === pinNumber,
  );
  if (!port) throw new Error(`Missing ${componentName} pin ${pinNumber}`);
  return port;
};

const getSchematicComponent = (
  circuitJson: AnyCircuitElement[],
  componentName: string,
) => {
  const sourceComponent = getComponent(circuitJson, componentName);
  const schematicComponent = circuitJson.find(
    (element): element is SchematicComponent =>
      element.type === "schematic_component" &&
      element.source_component_id === sourceComponent.source_component_id,
  );
  if (!schematicComponent) {
    throw new Error(`Missing schematic component ${componentName}`);
  }
  return schematicComponent;
};

const getSchematicPort = (
  circuitJson: AnyCircuitElement[],
  componentName: string,
  pinNumber: number,
) => {
  const sourceComponent = getComponent(circuitJson, componentName);
  const sourcePort = circuitJson.find(
    (element): element is SourcePort =>
      element.type === "source_port" &&
      element.source_component_id === sourceComponent.source_component_id &&
      element.pin_number === pinNumber,
  );
  if (!sourcePort) {
    throw new Error(`Missing source port ${componentName}.${pinNumber}`);
  }
  const schematicPort = circuitJson.find(
    (element): element is SchematicPort =>
      element.type === "schematic_port" &&
      element.source_port_id === sourcePort.source_port_id,
  );
  if (!schematicPort) {
    throw new Error(`Missing schematic port ${componentName}.${pinNumber}`);
  }
  return schematicPort;
};

const getProjectedSchematicPort = (
  circuitJson: AnyCircuitElement[],
  representationName: string,
  symbolPinNumber: number,
) => {
  const schematicComponent = getSchematicComponent(
    circuitJson,
    representationName,
  );
  const schematicPort = circuitJson.find(
    (element): element is SchematicPort =>
      element.type === "schematic_port" &&
      element.schematic_component_id ===
        schematicComponent.schematic_component_id &&
      element.pin_number === symbolPinNumber,
  );
  if (!schematicPort) {
    throw new Error(
      `Missing projected schematic port ${representationName}.${symbolPinNumber}`,
    );
  }
  return schematicPort;
};

const hasNativeNetTrace = (
  circuitJson: AnyCircuitElement[],
  sourcePortId: string,
  netName: string,
) => {
  const sourceNetIds = new Set(
    circuitJson
      .filter(
        (element): element is SourceNet =>
          element.type === "source_net" && element.name === netName,
      )
      .map((net) => net.source_net_id),
  );
  return circuitJson.some(
    (element) =>
      element.type === "source_trace" &&
      element.connected_source_port_ids.includes(sourcePortId) &&
      element.connected_source_net_ids.some((netId) => sourceNetIds.has(netId)),
  );
};

const getPortSide = (
  circuitJson: AnyCircuitElement[],
  componentName: string,
  pinNumber: number,
) => {
  const componentCenter = getSchematicComponent(
    circuitJson,
    componentName,
  ).center;
  const portCenter = getSchematicPort(
    circuitJson,
    componentName,
    pinNumber,
  ).center;
  const xDelta = portCenter.x - componentCenter.x;
  const yDelta = portCenter.y - componentCenter.y;
  if (Math.abs(xDelta) > Math.abs(yDelta)) {
    return xDelta < 0 ? "left" : "right";
  }
  return yDelta < 0 ? "bottom" : "top";
};

const getProjectedPortSide = (
  circuitJson: AnyCircuitElement[],
  representationName: string,
  symbolPinNumber: number,
) => {
  const componentCenter = getSchematicComponent(
    circuitJson,
    representationName,
  ).center;
  const portCenter = getProjectedSchematicPort(
    circuitJson,
    representationName,
    symbolPinNumber,
  ).center;
  const xDelta = portCenter.x - componentCenter.x;
  const yDelta = portCenter.y - componentCenter.y;
  if (Math.abs(xDelta) > Math.abs(yDelta)) {
    return xDelta < 0 ? "left" : "right";
  }
  return yDelta < 0 ? "bottom" : "top";
};

const roundedCenter = (center: { x: number; y: number }) => ({
  x: Number(center.x.toFixed(4)),
  y: Number(center.y.toFixed(4)),
});

const expectOneNet = (
  circuitJson: AnyCircuitElement[],
  pins: ReadonlyArray<readonly [componentName: string, pinNumber: number]>,
) => {
  const keys = pins.map(([componentName, pinNumber]) =>
    getConnectivityKey(circuitJson, componentName, pinNumber),
  );
  expect(new Set(keys).size).toBe(1);
};

test("uses the authoritative devices, values, and one coordinate transform", () => {
  expect(TIDA01421_ALTIUM_SCALE).toBe(0.028);
  expect(tida01421Position(1280, 910, TIDA01421_SIGNAL_CHAIN_ORIGIN)).toEqual({
    schX: 12.88,
    schY: 1.96,
  });

  expect(getComponent(signalJson, "J1").manufacturer_part_number).toBe(
    "1727010",
  );
  expect(getComponent(signalJson, "R6")).toMatchObject({ resistance: 0.003 });
  expect(getComponent(signalJson, "U2").manufacturer_part_number).toBe(
    "INA240A1QDRQ1",
  );
  expect(getComponent(signalJson, "U3").manufacturer_part_number).toBe(
    "TLV2316QDGKRQ1",
  );
  expect(getComponent(signalJson, "U1").manufacturer_part_number).toBe(
    "LMV7275IDCKRQ1",
  );
  expect(getComponent(signalJson, "C15")).toMatchObject({
    capacitance: 0.1e-6,
  });
  expect(getComponent(powerJson, "U4").manufacturer_part_number).toBe(
    "TPS7B6933QDBVRQ1",
  );
  expect(getComponent(powerJson, "U5").manufacturer_part_number).toBe(
    "TPS7B6950QDBVRQ1",
  );
});

test("preserves Altium placements with documented native-symbol projection offsets", () => {
  const signalCenters = {
    R6: [320, 890],
    R5: [370, 910],
    R7: [370, 870],
    C8: [400, 890],
    U2: [520, 900],
    C3: [520, 1000],
    C4: [550, 1000],
    R12: [670, 670],
    R20: [710, 650],
    C10: [750, 640],
    C7: [660, 920],
    R3: [720, 920],
    U3A: [790, 915.357143],
    U3B: [1080, 910.357143],
    C1: [790, 1040],
    R1: [790, 1000],
    C5: [860, 1000],
    C6: [890, 1000],
    R11: [670, 840],
    R17: [670, 790],
    R8: [930, 915],
    R9: [930, 890],
    C9: [980, 860],
    R10: [1010, 860],
    R2: [1050, 980],
    U1Symbol: [1280, 905.357143],
    C2: [1280, 1010],
    R15: [1200, 830],
    R18: [1200, 780],
    R16: [1300, 800],
    R4: [1400, 922.857143],
    C15: [1460, 884.285714],
  } as const;
  const powerCenters = {
    U4: [790, 440],
    U5: [790, 280],
    C11: [690, 440],
    C12: [900, 440],
    C13: [690, 280],
    C14: [900, 280],
  } as const;
  const expectedCenters = (
    centers: Record<string, readonly [number, number]>,
    origin: { x: number; y: number },
  ) =>
    Object.fromEntries(
      Object.entries(centers).map(([name, [x, y]]) => {
        const { schX, schY } = tida01421Position(x, y, origin);
        return [name, roundedCenter({ x: schX, y: schY })];
      }),
    );
  const renderedCenters = (
    circuitJson: AnyCircuitElement[],
    centers: Record<string, readonly [number, number]>,
  ) =>
    Object.fromEntries(
      Object.keys(centers).map((name) => [
        name,
        roundedCenter(getSchematicComponent(circuitJson, name).center),
      ]),
    );

  expect(renderedCenters(signalJson, signalCenters)).toEqual(
    expectedCenters(signalCenters, TIDA01421_SIGNAL_CHAIN_ORIGIN),
  );
  expect(renderedCenters(powerJson, powerCenters)).toEqual(
    expectedCenters(powerCenters, TIDA01421_POWER_ORIGIN),
  );

  // Rectangular devices use the native chip-box renderer. Exact custom port
  // coordinates are intentionally not asserted because no custom box symbol
  // is supplied; pin sides and physical pin mappings are asserted below.
  expect(getSchematicComponent(signalJson, "J1").symbol_name).toBeFalsy();
  expect(getSchematicComponent(signalJson, "U2").symbol_name).toBeFalsy();
  expect(getSchematicComponent(powerJson, "U4").symbol_name).toBeFalsy();
  expect(getSchematicComponent(powerJson, "U5").symbol_name).toBeFalsy();

  const signalPinSides = {
    J1: { 1: "right", 2: "right" },
    R6: { 1: "bottom", 2: "top" },
    R5: { 1: "left", 2: "right" },
    R7: { 1: "left", 2: "right" },
    C8: { 1: "top", 2: "bottom" },
    U2: {
      1: "left",
      2: "right",
      3: "right",
      4: "left",
      5: "right",
      6: "left",
      7: "right",
      8: "left",
    },
    C3: { 1: "top", 2: "bottom" },
    C4: { 1: "top", 2: "bottom" },
    R12: { 1: "left", 2: "right" },
    R20: { 1: "bottom", 2: "top" },
    C10: { 1: "top", 2: "bottom" },
    C7: { 1: "right", 2: "left" },
    R3: { 1: "left", 2: "right" },
    C1: { 1: "right", 2: "left" },
    R1: { 1: "left", 2: "right" },
    C5: { 1: "top", 2: "bottom" },
    C6: { 1: "top", 2: "bottom" },
    R11: { 1: "bottom", 2: "top" },
    R17: { 1: "bottom", 2: "top" },
    R8: { 1: "left", 2: "right" },
    R9: { 1: "left", 2: "right" },
    C9: { 1: "top", 2: "bottom" },
    R10: { 1: "bottom", 2: "top" },
    R2: { 1: "left", 2: "right" },
    C2: { 1: "top", 2: "bottom" },
    R15: { 1: "bottom", 2: "top" },
    R18: { 1: "bottom", 2: "top" },
    R16: { 1: "left", 2: "right" },
    R4: { 1: "bottom", 2: "top" },
    C15: { 1: "top", 2: "bottom" },
  } as const;
  const powerPinSides = {
    U4: { 1: "left", 2: "left", 3: "right", 4: "right", 5: "right" },
    U5: { 1: "left", 2: "left", 3: "right", 4: "right", 5: "right" },
    C11: { 1: "top", 2: "bottom" },
    C12: { 1: "top", 2: "bottom" },
    C13: { 1: "top", 2: "bottom" },
    C14: { 1: "top", 2: "bottom" },
  } as const;
  const renderedPinSides = (
    circuitJson: AnyCircuitElement[],
    expected: Record<string, Record<number, string>>,
  ) =>
    Object.fromEntries(
      Object.entries(expected).map(([name, pins]) => [
        name,
        Object.fromEntries(
          Object.keys(pins).map((pin) => [
            pin,
            getPortSide(circuitJson, name, Number(pin)),
          ]),
        ),
      ]),
    );

  expect(renderedPinSides(signalJson, signalPinSides)).toEqual(signalPinSides);
  expect(renderedPinSides(powerJson, powerPinSides)).toEqual(powerPinSides);

  const projectionPinSides = {
    U3A: { 1: "left", 2: "left", 3: "bottom", 4: "right", 5: "top" },
    U3B: { 1: "left", 2: "left", 3: "bottom", 4: "right", 5: "top" },
    U1Symbol: {
      1: "left",
      2: "left",
      3: "bottom",
      4: "right",
      5: "top",
    },
  } as const;
  const renderedProjectionPinSides = Object.fromEntries(
    Object.entries(projectionPinSides).map(([name, pins]) => [
      name,
      Object.fromEntries(
        Object.keys(pins).map((pin) => [
          pin,
          getProjectedPortSide(signalJson, name, Number(pin)),
        ]),
      ),
    ]),
  );
  expect(renderedProjectionPinSides).toEqual(projectionPinSides);
});

test("keeps source-aligned passive groups and the TIMER route straight", () => {
  const nearlyEqual = (a: number, b: number) => Math.abs(a - b) < 1e-6;
  const u3aOutput = getProjectedSchematicPort(signalJson, "U3A", 4);
  const r8Input = getSchematicPort(signalJson, "R8", 1);
  const r8Output = getSchematicPort(signalJson, "R8", 2);
  const u1Output = getProjectedSchematicPort(signalJson, "U1Symbol", 4);
  const r4TimerPin = getSchematicPort(signalJson, "R4", 1);
  const c15TimerPin = getSchematicPort(signalJson, "C15", 1);

  expect(
    nearlyEqual(
      getSchematicComponent(signalJson, "C1").center.x,
      getSchematicComponent(signalJson, "R1").center.x,
    ),
  ).toBe(true);
  expect(
    nearlyEqual(
      getSchematicPort(signalJson, "C1", 1).center.x,
      getSchematicPort(signalJson, "R1", 2).center.x,
    ),
  ).toBe(true);
  expect(
    nearlyEqual(
      getSchematicPort(signalJson, "C1", 2).center.x,
      getSchematicPort(signalJson, "R1", 1).center.x,
    ),
  ).toBe(true);

  for (const [first, second] of [
    ["C3", "C4"],
    ["C5", "C6"],
    ["C9", "R10"],
  ] as const) {
    expect(
      nearlyEqual(
        getSchematicComponent(signalJson, first).center.y,
        getSchematicComponent(signalJson, second).center.y,
      ),
    ).toBe(true);
  }

  for (const [first, second] of [
    ["R11", "R17"],
    ["R8", "R9"],
    ["R15", "R18"],
    ["U1Symbol", "C2"],
  ] as const) {
    expect(
      nearlyEqual(
        getSchematicComponent(signalJson, first).center.x,
        getSchematicComponent(signalJson, second).center.x,
      ),
    ).toBe(true);
  }

  expect(nearlyEqual(u3aOutput.center.y, r8Input.center.y)).toBe(true);
  expect(nearlyEqual(r8Input.center.y, r8Output.center.y)).toBe(true);

  const schematicEdges = signalJson
    .filter(
      (element): element is SchematicTrace =>
        element.type === "schematic_trace",
    )
    .flatMap((trace) => trace.edges);
  const hasHorizontalEdge = (fromX: number, toX: number, y: number) =>
    schematicEdges.some(
      (edge) =>
        nearlyEqual(edge.from.y, y) &&
        nearlyEqual(edge.to.y, y) &&
        nearlyEqual(Math.min(edge.from.x, edge.to.x), Math.min(fromX, toX)) &&
        nearlyEqual(Math.max(edge.from.x, edge.to.x), Math.max(fromX, toX)),
    );

  expect(
    hasHorizontalEdge(
      u1Output.center.x,
      r4TimerPin.center.x,
      u1Output.center.y,
    ),
  ).toBe(true);
  expect(
    hasHorizontalEdge(
      r4TimerPin.center.x,
      c15TimerPin.center.x,
      u1Output.center.y,
    ),
  ).toBe(true);
});

test("maps each native amplifier symbol port to the authoritative physical pin", () => {
  const disconnectedProjectionPorts: string[] = [];
  const expectedProjectionMappings = {
    U3A: {
      1: ["U3", 3],
      2: ["U3", 2],
      3: ["U3", 4],
      4: ["U3", 1],
      5: ["U3", 8],
    },
    U3B: {
      1: ["U3", 5],
      2: ["U3", 6],
      3: ["U3", 4],
      4: ["U3", 7],
      5: ["U3", 8],
    },
    U1Symbol: {
      1: ["U1", 1],
      2: ["U1", 3],
      3: ["U1", 2],
      4: ["U1", 4],
      5: ["U1", 5],
    },
  } as const;

  for (const [representationName, mappings] of Object.entries(
    expectedProjectionMappings,
  )) {
    expect(
      getSchematicComponent(signalJson, representationName).symbol_name,
    ).toBe("opamp_with_power_right");
    for (const [
      symbolPinNumber,
      [physicalComponent, physicalPinNumber],
    ] of Object.entries(mappings)) {
      const projectedPort = getProjectedSchematicPort(
        signalJson,
        representationName,
        Number(symbolPinNumber),
      );
      expect(projectedPort.source_port_id).toBe(
        getSourcePort(signalJson, physicalComponent, physicalPinNumber)
          .source_port_id,
      );
      const isSignalPin = ![3, 5].includes(Number(symbolPinNumber));
      if (isSignalPin && !projectedPort.is_connected)
        disconnectedProjectionPorts.push(
          `${representationName}.${symbolPinNumber}`,
        );
    }
  }
  expect(disconnectedProjectionPorts).toEqual([]);

  const supplyLabels = {
    U3A: { 3: "GND", 5: "V5" },
    U3B: { 3: "GND", 5: "V5" },
    U1Symbol: { 3: "GND", 5: "V5" },
  } as const;
  for (const [representationName, pins] of Object.entries(supplyLabels)) {
    for (const [symbolPinNumber, netName] of Object.entries(pins)) {
      const projectedPort = getProjectedSchematicPort(
        signalJson,
        representationName,
        Number(symbolPinNumber),
      );
      expect(
        hasNativeNetTrace(signalJson, projectedPort.source_port_id, netName),
      ).toBe(true);
    }
  }
});

test("routes every reference polarity net to the matching visible symbol port", () => {
  const expectedPolarities = {
    U3A: {
      plus: ["U3", 3],
      minus: ["U3", 2],
      plusNet: [
        ["U3", 3],
        ["R11", 1],
        ["R17", 2],
        ["R10", 1],
      ],
      minusNet: [
        ["U3", 2],
        ["R3", 2],
        ["R1", 1],
        ["C1", 2],
      ],
    },
    U3B: {
      plus: ["U3", 5],
      minus: ["U3", 6],
      plusNet: [
        ["U3", 5],
        ["R9", 2],
        ["C9", 1],
        ["R10", 2],
      ],
      minusNet: [
        ["U3", 6],
        ["R8", 2],
        ["R2", 1],
      ],
    },
    U1Symbol: {
      plus: ["U1", 1],
      minus: ["U1", 3],
      plusNet: [
        ["U1", 1],
        ["R15", 1],
        ["R18", 2],
        ["R16", 1],
      ],
      minusNet: [
        ["U1", 3],
        ["U3", 7],
        ["R2", 2],
      ],
    },
  } as const;

  for (const [representationName, polarity] of Object.entries(
    expectedPolarities,
  )) {
    const visiblePlusPort = getProjectedSchematicPort(
      signalJson,
      representationName,
      1,
    );
    const visibleMinusPort = getProjectedSchematicPort(
      signalJson,
      representationName,
      2,
    );

    expect(visiblePlusPort.source_port_id).toBe(
      getSourcePort(signalJson, polarity.plus[0], polarity.plus[1])
        .source_port_id,
    );
    expect(visibleMinusPort.source_port_id).toBe(
      getSourcePort(signalJson, polarity.minus[0], polarity.minus[1])
        .source_port_id,
    );
    expectOneNet(signalJson, [...polarity.plusNet]);
    expectOneNet(signalJson, [...polarity.minusNet]);
  }
});

test("preserves the shunt, ripple-filter, comparator, and output nets", () => {
  const expectedAltiumNets: Array<
    Array<[componentName: string, pinNumber: number]>
  > = [
    [
      ["J1", 1],
      ["R6", 2],
      ["R5", 1],
    ],
    [
      ["J1", 2],
      ["R6", 1],
      ["R7", 1],
    ],
    [
      ["R5", 2],
      ["C8", 1],
      ["U2", 8],
    ],
    [
      ["R7", 2],
      ["C8", 2],
      ["U2", 1],
    ],
    [
      ["U2", 5],
      ["C7", 2],
      ["R12", 1],
    ],
    [
      ["C7", 1],
      ["R3", 1],
    ],
    [
      ["R3", 2],
      ["C1", 2],
      ["R1", 1],
      ["U3", 2],
    ],
    [
      ["C1", 1],
      ["R1", 2],
      ["U3", 1],
      ["R8", 1],
      ["R9", 1],
    ],
    [
      ["R11", 1],
      ["R17", 2],
      ["U3", 3],
      ["R10", 1],
    ],
    [
      ["R8", 2],
      ["R2", 1],
      ["U3", 6],
    ],
    [
      ["R9", 2],
      ["C9", 1],
      ["R10", 2],
      ["U3", 5],
    ],
    [
      ["R2", 2],
      ["U3", 7],
      ["U1", 3],
    ],
    [
      ["R15", 1],
      ["R18", 2],
      ["R16", 1],
      ["U1", 1],
    ],
    [
      ["U1", 4],
      ["R16", 2],
      ["R4", 1],
      ["C15", 1],
    ],
    [
      ["R12", 2],
      ["R20", 2],
      ["C10", 1],
    ],
    [
      ["U2", 6],
      ["U2", 7],
      ["C3", 1],
      ["C4", 1],
      ["U3", 8],
      ["C5", 1],
      ["C6", 1],
      ["R11", 2],
      ["U1", 5],
      ["C2", 1],
      ["R15", 2],
    ],
    [
      ["U2", 2],
      ["U2", 3],
      ["C3", 2],
      ["C4", 2],
      ["C10", 2],
      ["R20", 1],
      ["R17", 1],
      ["U3", 4],
      ["C5", 2],
      ["C6", 2],
      ["C9", 2],
      ["U1", 2],
      ["C2", 2],
      ["R18", 1],
      ["C15", 2],
    ],
    [["R4", 2]],
  ];

  for (const pins of expectedAltiumNets) expectOneNet(signalJson, pins);
  const representativeKeys = expectedAltiumNets.map((pins) =>
    getConnectivityKey(signalJson, pins[0][0], pins[0][1]),
  );
  expect(new Set(representativeKeys).size).toBe(expectedAltiumNets.length);
});

test("preserves the local power nets and renders without circuit errors", () => {
  expectOneNet(powerJson, [
    ["U4", 1],
    ["U5", 1],
    ["C11", 1],
    ["C13", 1],
  ]);
  expectOneNet(powerJson, [
    ["U4", 5],
    ["C12", 1],
  ]);
  expectOneNet(powerJson, [
    ["U5", 5],
    ["C14", 1],
  ]);
  expectOneNet(powerJson, [
    ["U4", 3],
    ["U4", 4],
    ["U5", 3],
    ["U5", 4],
    ["C11", 2],
    ["C12", 2],
    ["C13", 2],
    ["C14", 2],
  ]);
  expectOneNet(signalJson, [
    ["U2", 2],
    ["C3", 2],
    ["C10", 2],
    ["R17", 1],
    ["U3", 4],
    ["C5", 2],
    ["C9", 2],
    ["U1", 2],
    ["C2", 2],
    ["R18", 1],
    ["C15", 2],
  ]);

  const circuitErrors = [...signalJson, ...powerJson].filter((element) =>
    element.type.endsWith("_error"),
  );
  expect(circuitErrors).toEqual([]);

  expect(
    powerJson
      .filter(
        (element): element is SourceTrace => element.type === "source_trace",
      )
      .map((trace) => trace.name)
      .filter((name) => name?.startsWith("GND_"))
      .sort(),
  ).toEqual(["GND_C11_pin2", "GND_C12_pin2", "GND_C13_pin2", "GND_C14_pin2"]);
});

test("uses named traces for source signals without explicit net-label components", () => {
  const explicitNetNames = signalJson
    .filter((element) => element.type === "source_net")
    .map((net) => net.name)
    .sort();
  expect(explicitNetNames).toEqual([
    "ADCMOTOR",
    "BIAS",
    "GND",
    "TIMER",
    "V3_3",
    "V5",
    "V_MINUS",
    "V_PLUS",
  ]);

  const namedSignalTraces = signalJson
    .filter(
      (element): element is SourceTrace => element.type === "source_trace",
    )
    .map((trace) => trace.name)
    .filter(Boolean)
    .sort();
  expect(namedSignalTraces).toEqual(
    expect.arrayContaining([
      "ADCMOTOR",
      "BIAS-A",
      "BIAS-B",
      "TIMER",
      "V-MINUS-INPUT",
      "V-PLUS-INPUT",
    ]),
  );

  const motorInputNets = Object.fromEntries(
    signalJson
      .filter(
        (element): element is SourceNet =>
          element.type === "source_net" &&
          ["V_PLUS", "V_MINUS"].includes(element.name),
      )
      .map((net) => [net.name, net]),
  );
  expect(motorInputNets.V_PLUS).toMatchObject({
    is_power: true,
  });
  expect(motorInputNets.V_MINUS).toMatchObject({
    is_ground: true,
  });

  for (const [traceName, netName] of [
    ["V-PLUS-INPUT", "V_PLUS"],
    ["V-MINUS-INPUT", "V_MINUS"],
  ] as const) {
    const trace = signalJson.find(
      (element): element is SourceTrace =>
        element.type === "source_trace" && element.name === traceName,
    );
    expect(trace?.connected_source_net_ids).toEqual([
      motorInputNets[netName].source_net_id,
    ]);
  }

  expect(
    signalJson.filter(
      (element): element is SchematicText =>
        element.type === "schematic_text" &&
        Boolean(element.source_trace_id) &&
        element.text === "V5",
    ),
  ).toEqual([]);

  for (const pinNumber of [6, 7]) {
    expect(
      hasNativeNetTrace(
        signalJson,
        getSourcePort(signalJson, "U2", pinNumber).source_port_id,
        "V5",
      ),
    ).toBe(true);
  }

  const traceOwnedSignalLabels = signalJson.filter(
    (element): element is SchematicText =>
      element.type === "schematic_text" &&
      Boolean(element.source_trace_id) &&
      ["ADCMOTOR", "BIAS", "TIMER"].includes(element.text),
  );
  expect(
    [...new Set(traceOwnedSignalLabels.map((label) => label.text))].sort(),
  ).toEqual(["ADCMOTOR", "BIAS", "TIMER"]);

  const motorInputTraceLabels = signalJson.filter(
    (element): element is SchematicText =>
      element.type === "schematic_text" &&
      Boolean(element.source_trace_id) &&
      ["V+", "V-"].includes(element.text),
  );
  // The published core version intentionally used by this package does not
  // render schDisplayLabel text inline on a branched trace. Once the required
  // source-net metadata is present, it projects the selector-safe source-net
  // names at the endpoints; the TSX still contains no netlabel component.
  expect(motorInputTraceLabels).toEqual([]);
  const generatedMotorInputLabels = signalJson.filter(
    (element): element is SchematicNetLabel =>
      element.type === "schematic_net_label" &&
      ["V_PLUS", "V_MINUS"].includes(element.text),
  );
  expect(
    generatedMotorInputLabels
      .map((label) => [label.text, label.symbol_name, label.source_trace_id])
      .sort(),
  ).toEqual([
    ["V_MINUS", "rail_down", undefined],
    ["V_PLUS", "rail_up", undefined],
  ]);
  expect(
    signalJson.filter(
      (element) =>
        element.type === "schematic_net_label" &&
        ["ADCMOTOR", "BIAS", "TIMER"].includes(element.text),
    ),
  ).toEqual([]);

  const renderedLabels = [
    ...new Set(
      signalJson.flatMap((element) => {
        if (element.type === "schematic_net_label") return [element.text];
        if (element.type === "schematic_text" && element.source_trace_id)
          return [element.text];
        return [];
      }),
    ),
  ].sort();
  expect(renderedLabels).toEqual([
    "ADCMOTOR",
    "BIAS",
    "GND",
    "TIMER",
    "V3_3",
    "V5",
    "V_MINUS",
    "V_PLUS",
  ]);
});

test("joins the child power rails in the composite without circuit errors", () => {
  for (const netName of ["V3_3", "V5", "GND"]) {
    const exposedNetKeys = compositeJson
      .filter(
        (element): element is SourceNet =>
          element.type === "source_net" && element.name === netName,
      )
      .map((net) => net.subcircuit_connectivity_map_key);
    // One source net exists in each child and the wrapper. Native exposedNets
    // remaps all three to the wrapper connectivity key.
    expect(exposedNetKeys).toHaveLength(3);
    expect(new Set(exposedNetKeys).size).toBe(1);
  }

  expect(
    compositeJson.filter((element) => element.type.endsWith("_error")),
  ).toEqual([]);

  expect(
    compositeJson.filter(
      (element) =>
        element.type === "schematic_net_label" &&
        (/(?:^|[_-])(?:U\d+[A-Z]?[_-])?(?:V5|GND)$/.test(element.text) ||
          element.text === "U2_VS") &&
        !["V5", "GND"].includes(element.text),
    ),
  ).toEqual([]);

  const compositeSupplyLabels = {
    U3A: { 3: "GND", 5: "V5" },
    U3B: { 3: "GND", 5: "V5" },
    U1Symbol: { 3: "GND", 5: "V5" },
  } as const;
  for (const [representationName, pins] of Object.entries(
    compositeSupplyLabels,
  )) {
    for (const [symbolPinNumber, netName] of Object.entries(pins)) {
      const projectedPort = getProjectedSchematicPort(
        compositeJson,
        representationName,
        Number(symbolPinNumber),
      );
      expect(
        hasNativeNetTrace(compositeJson, projectedPort.source_port_id, netName),
      ).toBe(true);
    }
  }
});