Merge pull request 'dev' (#115) from dev into main

Reviewed-on: #115
This commit was merged in pull request #115.
This commit is contained in:
2026-06-09 13:15:48 +00:00
3 changed files with 116 additions and 8 deletions

View File

@@ -207,10 +207,92 @@ describe("CategoriesService", () => {
expect(service.getChildren).toHaveBeenCalledWith("c1"); expect(service.getChildren).toHaveBeenCalledWith("c1");
expect(partsCatalogsService.fetchParts).not.toHaveBeenCalled(); expect(partsCatalogsService.fetchParts).not.toHaveBeenCalled();
expect(result.parts).toEqual([]); expect(result.parts).toEqual([]);
expect(result.children).toEqual(discovered); expect((result as { children?: unknown }).children).toEqual(discovered);
// The wrapper attaches the server-resolved ancestor trail (id+name only). // The wrapper attaches the server-resolved ancestor trail (id+name only).
expect(result.ancestors).toEqual([{ id: "root", name: "Kök" }]); expect(result.ancestors).toEqual([{ id: "root", name: "Kök" }]);
}); });
// Regression guard for the PSA "0 parça" bug: VIN-decoded Peugeot/Citroën
// scope categories carry a json-vin-main-groups.action linkPath (and the
// next level json-vin-illustrations.action). These are PARENTS — they must
// route to getChildren, not fall through to the leaf path and render empty.
// The dispatch must stay in sync with PL24Service.isPsaVin*Path; this test
// fails loudly if it drifts again.
it("routes a PSA VIN scope (json-vin-main-groups.action) to children, not an empty leaf", async () => {
const category = {
id: "psa1",
name: "mekanik",
nameOriginal: "mekanik",
parentId: null,
vehicleId: "v1",
source: "pl24",
linkPath: "/psa/peugeot_parts/json-vin-main-groups.action?scope=_FCT0001&vin=VF3ABCDE",
linkWid: null,
hasSubgroups: null,
hasParts: null,
};
let selectCall = 0;
const db = {
select: vi.fn().mockImplementation(() => {
selectCall++;
const captured = selectCall;
const c: Record<string, any> = {};
c.from = vi.fn().mockReturnValue(c);
c.where = vi.fn().mockImplementation(() => (captured === 2 ? [] : c));
c.limit = vi.fn().mockReturnValue([category]);
return c;
}),
execute: vi.fn().mockResolvedValue([]),
};
const { service } = createService(db);
const discovered = [{ id: "mg1", name: "motor", children: undefined }];
vi.spyOn(service, "getChildren").mockResolvedValue(discovered as any);
const result = await service.getCategoryWithParts("psa1");
expect(service.getChildren).toHaveBeenCalledWith("psa1");
expect(result.parts).toEqual([]);
expect((result as { children?: unknown }).children).toEqual(discovered);
});
it("surfaces loadError (not an empty grid) when a PSA VIN main-group drill comes back empty", async () => {
const category = {
id: "psa2",
name: "motor",
nameOriginal: "motor",
parentId: "psa1",
vehicleId: "v1",
source: "pl24",
linkPath: "/psa/peugeot_parts/json-vin-illustrations.action?mainGroup=P8&vin=VF3ABCDE",
linkWid: null,
hasSubgroups: null,
hasParts: null,
};
let selectCall = 0;
const db = {
select: vi.fn().mockImplementation(() => {
selectCall++;
const captured = selectCall;
const c: Record<string, any> = {};
c.from = vi.fn().mockReturnValue(c);
c.where = vi.fn().mockImplementation(() => (captured === 2 ? [] : c));
c.limit = vi.fn().mockReturnValue([category]);
return c;
}),
execute: vi.fn().mockResolvedValue([]),
};
const { service } = createService(db);
// Drill comes back empty (transient upstream failure).
vi.spyOn(service, "getChildren").mockResolvedValue([] as any);
const result = await service.getCategoryWithParts("psa2");
expect(service.getChildren).toHaveBeenCalledWith("psa2");
expect((result as { loadError?: boolean }).loadError).toBe(true);
expect((result as { children?: unknown }).children).toBeUndefined();
});
}); });
describe("getAncestors", () => { describe("getAncestors", () => {

View File

@@ -918,15 +918,29 @@ export class CategoriesService {
} }
// PSA parent nodes — always fetch children on-demand: // PSA parent nodes — always fetch children on-demand:
// • psa:: scope paths (top-level scopes) // • psa:: scope paths (catalog-browse top-level scopes)
// • json-illustrations.action paths (mid-level main groups → illustration lists) // • json-illustrations.action (catalog-browse main group → illustrations)
// • json-vin-main-groups.action (VIN-indexed scope → main groups)
// • json-vin-illustrations.action (VIN-indexed main group → illustrations)
// The two VIN-indexed levels were MISSING here, so VIN-decoded Peugeot/Citroën
// scope categories ("mekanik", "kaporta", "ekipmanlar"…) fell through every
// parent branch to the leaf path, fetched no parts (the main-groups endpoint
// returns groups, not parts) and rendered an empty panel — the single biggest
// "0 parça" complaint. The drill itself works: getChildren →
// fetchSubGroupsByPath → fetchVinMainGroups / fetchVinIllustrations. These four
// predicates must stay in sync with PL24Service.isPsaPath / isPsaIllusPath /
// isPsaVinMainGroupsPath / isPsaVinIllusPath; the leaf level
// (vin-image-board.action / image-board.action) correctly stays a leaf below.
const psaLink = category.linkPath ?? "";
const isPsaParent = const isPsaParent =
category.linkPath?.startsWith("psa::") || psaLink.startsWith("psa::") ||
(category.linkPath?.includes("/psa/") && (psaLink.includes("/psa/") &&
category.linkPath?.includes("json-illustrations.action")); (psaLink.includes("json-illustrations.action") ||
psaLink.includes("json-vin-main-groups.action") ||
psaLink.includes("json-vin-illustrations.action")));
if (isPsaParent && category.vehicleId) { if (isPsaParent && category.vehicleId) {
const psaChildren = await this.getChildren(categoryId); const psaChildren = await this.getChildren(categoryId);
return { const base = {
id: category.id, id: category.id,
name: category.name, name: category.name,
description: category.nameOriginal || null, description: category.nameOriginal || null,
@@ -934,8 +948,14 @@ export class CategoriesService {
parts: [], parts: [],
schemaPics: [], schemaPics: [],
hotspots: [], hotspots: [],
children: psaChildren,
}; };
// A PSA scope / main group is never a parts leaf. If the on-demand drill
// comes back empty the upstream snapshot failed transiently — surface a
// retryable load error (matching the pl24/emex group-node branches below)
// instead of a misleading empty subcategory grid.
return psaChildren.length > 0
? { ...base, children: psaChildren }
: { ...base, loadError: true };
} }
// parts-catalogs parent group — the groups2 API marked this node as having // parts-catalogs parent group — the groups2 API marked this node as having

View File

@@ -57,6 +57,12 @@ async function bootstrap() {
], ],
objectSrc: ["'none'"], objectSrc: ["'none'"],
frameSrc: ["https://challenges.cloudflare.com", "https://destek.sase.tr"], frameSrc: ["https://challenges.cloudflare.com", "https://destek.sase.tr"],
// The Meta Pixel (fbevents 2.9.x) dispatches ALL events from a Web Worker
// created off a blob: URL. With no worker-src, the browser falls back to
// script-src — which lacks blob: — so the worker is CSP-blocked and NO
// pixel events fire (PageView, CompleteRegistration, everything were 0
// until this). Also unblocks Remotion's blob worker on the demos.
workerSrc: ["'self'", "blob:"],
}, },
}, },
}), }),