Skip to content

Commit 1d44f41

Browse files
committed
test: Add test for loading FeXAS example and rendering inline nested object properties
1 parent 7bd6ea7 commit 1d44f41

File tree

2 files changed

+156
-79
lines changed

2 files changed

+156
-79
lines changed

src/jsonld-editor/render.js

Lines changed: 125 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
11
// Author: Eryk Kulikowski @ KU Leuven (2025). Apache 2.0 License
2-
3-
// === CDI Previewer: Tree Rendering & Node/Property Display ===
4-
52
import {
63
getJsonData,
74
getIsEditMode,
@@ -93,11 +90,9 @@ export function renderData() {
9390

9491
// Helper: Check if a value is a pure reference (no properties other than @id, @type, @context)
9592
function isPureReference(value) {
96-
// String reference like "#Sample_Key"
9793
if (typeof value === "string" && isNodeReference(value)) {
9894
return true;
9995
}
100-
// Object reference like {"@id": "#Sample_Key"}
10196
if (value && typeof value === "object" && !Array.isArray(value)) {
10297
const keys = Object.keys(value);
10398
const nonMetadataKeys = keys.filter(
@@ -331,13 +326,129 @@ export function renderPropertyTree(
331326
container.append(childCard);
332327
}
333328
// else: Node already rendered elsewhere - the inline box already provides navigation
329+
} else {
330+
// The referenced ID wasn't found as a top-level node in the @graph.
331+
// This can happen when an object is inlined (has an @id plus nested
332+
// properties) inside the parent and isn't a separate graph node. In
333+
// that case we should find the inline object and render it inline so
334+
// nested fields are visible (instead of only showing a jump button).
335+
const tryInline = (val) => {
336+
if (!val) {
337+
return null;
338+
}
339+
if (Array.isArray(val)) {
340+
return val.find(
341+
(it) =>
342+
it &&
343+
typeof it === "object" &&
344+
it["@id"] === refId &&
345+
!isPureReference(it)
346+
);
347+
}
348+
if (
349+
typeof val === "object" &&
350+
val !== null &&
351+
val["@id"] === refId &&
352+
!isPureReference(val)
353+
) {
354+
return val;
355+
}
356+
return null;
357+
};
358+
359+
const inlineObj = tryInline(value);
360+
if (inlineObj) {
361+
// Track parent-child relationship using the inline object's @id
362+
setParentRelationship(refId, nodeId);
363+
364+
const inlineCard = renderInlineObject(inlineObj, nodeId);
365+
if (inlineCard) {
366+
// Append inline node view at same depth
367+
container.append(inlineCard);
368+
// Mark as rendered to avoid duplicate rendering
369+
markNodeRendered(refId);
370+
}
371+
}
334372
}
335373
});
336374
}
337375

338376
return container;
339377
}
340378

379+
// Top-level helper: render a nested inline object as a small inline node card.
380+
// Accepts optional parent node id for context when rendering nested properties.
381+
function renderInlineObject(val, parentNodeId) {
382+
if (!val || typeof val !== "object" || Array.isArray(val)) {
383+
return null;
384+
}
385+
386+
// Skip pure references - let them be rendered as clickable buttons
387+
if (isPureReference(val)) {
388+
return null;
389+
}
390+
391+
const inlineCard = $("<div>").addClass("node-card inline-node-card").css({
392+
"margin-top": "5px",
393+
"margin-bottom": "5px",
394+
});
395+
396+
const header = $("<div>").addClass("node-header");
397+
const leftSide = $("<div>")
398+
.css("display", "flex")
399+
.css("align-items", "center");
400+
401+
leftSide.append(
402+
$("<span>")
403+
.addClass("glyphicon glyphicon-chevron-down collapse-icon")
404+
.css("margin-right", "10px")
405+
);
406+
407+
const nestedId = val["@id"];
408+
if (nestedId) {
409+
leftSide.append($("<span>").addClass("node-id").text(nestedId));
410+
}
411+
412+
const nestedTypes = Array.isArray(val["@type"])
413+
? val["@type"]
414+
: val["@type"]
415+
? [val["@type"]]
416+
: [];
417+
nestedTypes.forEach((t) => {
418+
if (t) {
419+
leftSide.append($("<span>").addClass("node-type").text(t));
420+
}
421+
});
422+
423+
header.append(leftSide);
424+
header.click(function () {
425+
inlineCard.toggleClass("collapsed");
426+
});
427+
428+
inlineCard.append(header);
429+
430+
const body = $("<div>").addClass("node-body");
431+
if (!getIsEditMode()) {
432+
body.addClass("view-mode");
433+
}
434+
435+
Object.keys(val).forEach((k) => {
436+
if (k === "@id" || k === "@type" || k === "@context") {
437+
return;
438+
}
439+
const nestedRow = renderProperty(
440+
k,
441+
val[k],
442+
nestedId || parentNodeId,
443+
nestedTypes
444+
);
445+
body.append(nestedRow);
446+
});
447+
448+
inlineCard.append(body);
449+
return inlineCard;
450+
}
451+
341452
function renderProperty(key, value, nodeId, nodeTypes) {
342453
const row = $("<div>")
343454
.addClass("property-row")
@@ -403,85 +514,15 @@ function renderProperty(key, value, nodeId, nodeTypes) {
403514
.addClass("property-value")
404515
.attr("data-testid", "property-value");
405516

406-
// Helper: render a nested object value using a small inline node card
407-
function renderInlineObject(val) {
408-
if (!val || typeof val !== "object" || Array.isArray(val)) {
409-
return null;
410-
}
411-
412-
// Skip pure references - let them be rendered as clickable buttons
413-
if (isPureReference(val)) {
414-
return null;
415-
}
416-
417-
const inlineCard = $("").addClass("node-card inline-node-card").css({
418-
"margin-top": "5px",
419-
"margin-bottom": "5px",
420-
});
421-
422-
const header = $("<div>").addClass("node-header");
423-
const leftSide = $("<div>")
424-
.css("display", "flex")
425-
.css("align-items", "center");
426-
427-
leftSide.append(
428-
$("<span>")
429-
.addClass("glyphicon glyphicon-chevron-down collapse-icon")
430-
.css("margin-right", "10px")
431-
);
432-
433-
const nestedId = val["@id"];
434-
if (nestedId) {
435-
leftSide.append($("<span>").addClass("node-id").text(nestedId));
436-
}
437-
438-
const nestedTypes = Array.isArray(val["@type"])
439-
? val["@type"]
440-
: val["@type"]
441-
? [val["@type"]]
442-
: [];
443-
nestedTypes.forEach((t) => {
444-
if (t) {
445-
leftSide.append($("<span>").addClass("node-type").text(t));
446-
}
447-
});
448-
449-
header.append(leftSide);
450-
header.click(function () {
451-
inlineCard.toggleClass("collapsed");
452-
});
453-
454-
inlineCard.append(header);
455-
456-
const body = $("<div>").addClass("node-body");
457-
if (!getIsEditMode()) {
458-
body.addClass("view-mode");
459-
}
460-
461-
Object.keys(val).forEach((k) => {
462-
if (k === "@id" || k === "@type" || k === "@context") {
463-
return;
464-
}
465-
const nestedRow = renderProperty(
466-
k,
467-
val[k],
468-
nestedId || nodeId,
469-
nestedTypes
470-
);
471-
body.append(nestedRow);
472-
});
473-
474-
inlineCard.append(body);
475-
return inlineCard;
476-
}
517+
// Note: Uses top-level renderInlineObject helper for inlined object rendering
477518

478519
if (Array.isArray(value)) {
479520
// Array of values
480521
value.forEach((val, idx) => {
481522
const valDiv = $("<div>").addClass("array-value");
482523

483524
// Try to render nested objects (like schema:Role) as inline node cards
484-
const inlineCard = renderInlineObject(val);
525+
const inlineCard = renderInlineObject(val, nodeId);
485526
if (inlineCard) {
486527
valDiv.append(inlineCard);
487528
} else {
@@ -583,7 +624,7 @@ function renderProperty(key, value, nodeId, nodeTypes) {
583624
}
584625
} else {
585626
// Single value
586-
const inlineCard = renderInlineObject(value);
627+
const inlineCard = renderInlineObject(value, nodeId);
587628
if (inlineCard) {
588629
valueContainer.append(inlineCard);
589630
} else {
@@ -820,7 +861,12 @@ function showAddReferenceModal(
820861
export function createValueInput(value, classification) {
821862
// Check if value is a reference (either format)
822863
const refId = extractReferenceId(value);
823-
if (refId) {
864+
// Only treat as a clickable "reference" if this is a pure reference
865+
// (string-style '#id' or object with only @id and metadata). If the
866+
// value is an object that contains @id PLUS other properties, we
867+
// want to render it inline (showing its nested fields) instead of
868+
// hiding it behind a jump button.
869+
if (refId && isPureReference(value)) {
824870
const refContainer = $("<div>")
825871
.addClass("reference-container")
826872
.attr(

tests/e2e/standalone/file-loading.spec.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { test, expect } from '@playwright/test';
2+
import fs from 'fs';
23
import path from 'path';
34
import { fileURLToPath } from 'url';
45

@@ -197,6 +198,36 @@ test.describe('File Loading - Critical Path', () => {
197198
await expect(firstNode).not.toHaveClass(/collapsed/);
198199
});
199200

201+
test('should load FeXAS example and render inline nested object properties', async ({ page }) => {
202+
await page.goto('/');
203+
await page.waitForLoadState('domcontentloaded');
204+
await page.waitForSelector('#load-local-btn', { timeout: 10000 });
205+
206+
const testFilePath = path.join(__dirname, '../../../examples/cdi/FeXAS_Fe_c3d.001-NEXUS-HDF5-cdi-CDIF.jsonld');
207+
await page.click('#load-local-btn');
208+
await page.setInputFiles('#local-file-input', testFilePath);
209+
210+
// Wait for file to load and be processed
211+
await expect(page.locator('.alert-success')).toBeVisible({ timeout: 10000 });
212+
213+
// The example contains an embedded Organization with name 'APS' under schema:contributor
214+
// Verify that the nested property 'APS' appears somewhere in the rendered tree
215+
// Look for the string 'APS' anywhere inside the page (embedded organization name)
216+
const apsText = page.locator('text=APS');
217+
await expect(apsText.first()).toBeVisible({ timeout: 5000 });
218+
219+
// Also verify that deeply nested component names are shown (e.g. clock_mca4)
220+
const clockText = page.locator('text=clock_mca4');
221+
const count = await clockText.count();
222+
if (count === 0) {
223+
// Save page content for diagnosis
224+
const html = await page.content();
225+
fs.mkdirSync('tests/e2e/debug', { recursive: true });
226+
fs.writeFileSync('tests/e2e/debug/fexas_dom.html', html, 'utf8');
227+
}
228+
await expect(clockText.first()).toBeVisible({ timeout: 5000 });
229+
});
230+
200231
test('should load file without @context', async ({ page }) => {
201232
// ============= SETUP =============
202233
await page.goto('/');

0 commit comments

Comments
 (0)