Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 49 additions & 4 deletions lib/components/base-components/NormalComponent/NormalComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
point3,
rotation,
schematic_manual_edit_conflict_warning,
unknown_error_finding_part,
} from "circuit-json"
import { decomposeTSR } from "transformation-matrix"
import Debug from "debug"
Expand Down Expand Up @@ -1443,8 +1444,37 @@ export class NormalComponent<
}),
)

// Convert "Not found" to empty object before caching or returning
const supplierPartNumbers = result === "Not found" ? {} : result
// Validate the result format
if (typeof result === "string") {
// Check if it's an HTML error page or "Not found"
if (result.includes("<!DOCTYPE") || result.includes("<html")) {
throw new Error(
`Failed to fetch supplier part numbers: Received HTML response instead of JSON. Response starts with: ${result.substring(0, 100)}`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERY NICE HANDLING!!!!!!!!!

)
}
// Convert "Not found" to empty object
if (result === "Not found") {
return {}
}
throw new Error(
`Invalid supplier part numbers format: Expected object but got string: "${result}"`,
)
}

// Validate that result is an object (not array, null, etc.)
if (!result || Array.isArray(result) || typeof result !== "object") {
const actualType =
result === null
? "null"
: Array.isArray(result)
? "array"
: typeof result
throw new Error(
`Invalid supplier part numbers format: Expected object but got ${actualType}`,
)
}

const supplierPartNumbers = result

if (cacheEngine) {
try {
Expand Down Expand Up @@ -1483,8 +1513,23 @@ export class NormalComponent<
}

this._queueAsyncEffect("get-supplier-part-numbers", async () => {
this._asyncSupplierPartNumbers = await supplierPartNumbersMaybePromise
this._markDirty("PartsEngineRender")
await supplierPartNumbersMaybePromise
.then((supplierPartNumbers) => {
this._asyncSupplierPartNumbers = supplierPartNumbers
this._markDirty("PartsEngineRender")
})
.catch((error: Error) => {
// Log structured error to Circuit JSON
this._asyncSupplierPartNumbers = {}
const errorObj = unknown_error_finding_part.parse({
type: "unknown_error_finding_part",
message: `Failed to fetch supplier part numbers for ${this.getString()}: ${error.message}`,
source_component_id: this.source_component_id,
subcircuit_id: this.getSubcircuit()?.subcircuit_id,
})
db.unknown_error_finding_part.insert(errorObj)
this._markDirty("PartsEngineRender")
})
})
}

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
"bun-match-svg": "0.0.12",
"calculate-elbow": "^0.0.12",
"chokidar-cli": "^3.0.0",
"circuit-json": "^0.0.288",
"circuit-json": "^0.0.291",
"circuit-json-to-bpc": "^0.0.13",
"circuit-json-to-connectivity-map": "^0.0.22",
"circuit-json-to-gltf": "^0.0.31",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { test, expect } from "bun:test"
import { getTestFixture } from "tests/fixtures/get-test-fixture"

test("parts engine handles errors gracefully and logs them to Circuit JSON", async () => {
// Test 1: HTML error page
const { circuit: circuit1 } = getTestFixture()
const htmlMock = {
findPart: async () =>
"<!DOCTYPE html><html><body>Internal Server Error</body></html>",
} as any

circuit1.add(
<board partsEngine={htmlMock} width="20mm" height="20mm">
<resistor name="R1" resistance="10k" footprint="0402" />
</board>,
)
await circuit1.renderUntilSettled()

const sc1 = circuit1.db.source_component.list()[0]
expect(sc1.supplier_part_numbers).toEqual({})
const errors1 = circuit1.db.unknown_error_finding_part.list()
expect(errors1.length).toBeGreaterThan(0)
expect(errors1[0].message).toContain("Failed to fetch supplier part numbers")
expect(errors1[0].message).toContain("<!DOCTYPE")

// Test 2: Network failure
const { circuit: circuit2 } = getTestFixture()
const networkMock = {
findPart: async () => {
throw new Error("Network request failed")
},
}

circuit2.add(
<board partsEngine={networkMock} width="20mm" height="20mm">
<resistor name="R2" resistance="10k" footprint="0402" />
</board>,
)
await circuit2.renderUntilSettled()

const sc2 = circuit2.db.source_component.list()[0]
expect(sc2.supplier_part_numbers).toEqual({})
const errors2 = circuit2.db.unknown_error_finding_part.list()
expect(errors2.length).toBeGreaterThan(0)
expect(errors2[0].message).toContain("Network request failed")

// Test 3: Invalid format (array instead of object)
const { circuit: circuit3 } = getTestFixture()
const invalidMock = {
findPart: async () => ["123-456", "789-012"],
} as any

circuit3.add(
<board partsEngine={invalidMock} width="20mm" height="20mm">
<resistor name="R3" resistance="10k" footprint="0402" />
</board>,
)
await circuit3.renderUntilSettled()

const sc3 = circuit3.db.source_component.list()[0]
expect(sc3.supplier_part_numbers).toEqual({})
const errors3 = circuit3.db.unknown_error_finding_part.list()
expect(errors3.length).toBeGreaterThan(0)
expect(errors3[0].message).toContain("Invalid supplier part numbers format")
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { test, expect } from "bun:test"
import { getTestFixture } from "tests/fixtures/get-test-fixture"

test("parts engine processes valid responses and special cases correctly", async () => {
// Test 1: Valid supplier part numbers
const { circuit: circuit1 } = getTestFixture()
const validMock = {
findPart: async () => ({
digikey: ["123-456"],
mouser: ["789-012"],
}),
}

circuit1.add(
<board partsEngine={validMock} width="20mm" height="20mm">
<resistor name="R1" resistance="10k" footprint="0402" />
</board>,
)
await circuit1.renderUntilSettled()

const sc1 = circuit1.db.source_component.list()[0]
expect(sc1.supplier_part_numbers).toEqual({
digikey: ["123-456"],
mouser: ["789-012"],
})
const errors1 = circuit1.db.unknown_error_finding_part.list()
expect(errors1.length).toBe(0)

// Test 2: "Not found" response (valid, converts to empty object)
const { circuit: circuit2 } = getTestFixture()
const notFoundMock = {
findPart: async () => "Not found",
} as any

circuit2.add(
<board partsEngine={notFoundMock} width="20mm" height="20mm">
<resistor name="R2" resistance="10k" footprint="0402" />
</board>,
)
await circuit2.renderUntilSettled()

const sc2 = circuit2.db.source_component.list()[0]
expect(sc2.supplier_part_numbers).toEqual({})
const errors2 = circuit2.db.unknown_error_finding_part.list()
expect(errors2.length).toBe(0)
})