-
File Loading (7/7 tests - 100%) -
tests/e2e/standalone/file-loading.spec.ts- Load local JSON-LD file
- Load complex nested structure
- Load file without @context
- Handle invalid JSON-LD
- Load Schema.org dataset (generic mode)
- Verify validation auto-runs
- Handle edit mode toggle
- Handle search functionality
-
Validation (5/6 tests - 83%) -
tests/e2e/standalone/validation.spec.ts- Auto-validate on file load
- Debounce validation on rapid edits
- Show validation details on violations
- Validate after entering edit mode
- Not run validation in parallel
- Handle validation with different shape sources (FAILING - validation status becomes hidden after shape switch)
-
Editing (8/9 tests - 89%) -
tests/e2e/standalone/editing.spec.ts- Enable edit mode
- Disable edit mode
- Edit text property
- Handle empty text field
- Preserve required properties
- Delete optional property
- Mark changed properties visually
- Handle rapid edits with debounced validation
- Preserve "changed" marking when toggling edit mode (FAILING - visual marking disappears on mode toggle)
-
Search (8/8 tests - 100%) - Search functionality working correctly
- Highlight search matches in content
- Navigate between search matches
- Support case-sensitive search
- Support regex search
- Clear search and remove highlights
- Handle search with no results
- Persist search when toggling edit mode
- Keyboard shortcuts (F3, Shift+F3, Enter)
-
Namespace Management (6/9 tests - 67%) -
tests/e2e/standalone/namespace-management.spec.ts- Display existing namespaces
- Open add namespace modal
- Add custom namespace
- Validate prefix uniqueness (FAILING)
- Validate URI format
- Delete custom namespace (FAILING)
- Protected namespaces handling
- Edit namespace URI
- Toggle namespace section visibility (FAILING)
-
Array Operations (5/9 tests - 56%) -
tests/e2e/standalone/array-operations.spec.ts- Display array values correctly (FAILING)
- Add new value to array
- Delete array value
- Edit array value
- Convert single value to array (FAILING)
- Convert array to single value (FAILING)
- Handle array with object references
- Maintain array value order
- Validate array operations preserve data integrity (FAILING)
-
Export (5/8 tests - 63%) -
tests/e2e/standalone/export.spec.ts- Export JSON-LD data
- Export with pretty-print formatting
- Preserve user changes in export (FAILING)
- Include all namespaces in exported data
- Export without requiring edit mode
- Use correct MIME type for export (FAILING)
- Preserve property order in export
- Handle export with validation errors present (FAILING)
-
Custom Namespace Properties (7/12 tests - 58%) -
tests/e2e/standalone/custom-namespace-properties.spec.ts- Show custom namespace nodes in editor
- Display add properties section for custom namespace nodes
- Add custom property to custom namespace node using inline UI (FAILING - property not added)
- Add custom property without namespace to custom node (FAILING)
- Add multiple custom properties to same custom node (FAILING)
- Edit custom property value in custom namespace node (FAILING)
- Delete custom property from custom namespace node (FAILING)
- Add complex property (node reference) to custom namespace node
- Preserve custom properties when toggling edit mode (FAILING)
- Validate that custom property input is inline, not a popup ✅ BUG FIXED
- Handle Enter key in custom property input (FAILING)
- Show validation for empty custom property name
-
Custom Property UI (7/13 tests - 54%) -
tests/e2e/standalone/custom-property-ui.spec.ts- Use inline UI for adding custom properties, not popup ✅ BUG FIXED
- NOT have old-style popup button for adding custom properties ✅ BUG FIXED
- Nodes without SHACL suggestions should still have inline custom property UI ✅ BUG FIXED
- Custom property inline UI should match screenshot 1 ✅ BUG FIXED
- Custom property UI should be in a bordered section below property list
- Adding custom property via inline UI should work (FAILING - property not added)
- Adding custom property with namespace via inline UI should work (FAILING)
- Inline UI should clear input after adding property (FAILING)
- Inline UI should show validation message for empty property name
- Namespace selector should have 'Add new namespace' option
- All nodes should consistently use the same add property UI ✅ BUG FIXED
- Inline UI should support keyboard navigation (FAILING)
- Should not call browser prompt() when adding custom property ✅ BUG FIXED
-
Document Creation (0/8 tests - 0%) -
tests/e2e/standalone/document-creation.spec.ts- Create new DDI-CDI document - FAILING (selector or timing issues with Add Root Node dropdown)
- Create Schema.org document - FAILING (add namespace button timing)
- Add multiple root nodes - FAILING (Add Root Node functionality timing)
- Create document without shapes - FAILING (custom node type input selector)
- Create document with default properties - FAILING (Add Root Node dropdown timing)
- Auto-enable edit mode on document creation - FAILING (Add Root Node section not appearing)
- Preserve context when adding nodes - FAILING (namespace button timing)
- Generate default filename for new document - FAILING (Add Root Node timing)
- NOTE: Feature exists per screenshot - tests need selector/timing refinement
-
Dataverse Load (2/5 tests - 40%) -
tests/e2e/dataverse/load-from-dataverse.spec.ts- Load via URL parameters (integrated mode)
- Load via "Load from Dataverse" button - FAILING (button doesn't exist or different selector)
- Load with API token - FAILING (UI elements don't match expected selectors)
- Handle load error from Dataverse
- Parse multiple Dataverse URL formats - FAILING (UI elements don't match expected selectors)
-
Dataverse Save (1/5 tests - 20%) -
tests/e2e/dataverse/save-to-dataverse.spec.ts- Save button visible in integrated mode
- Save as new file option - FAILING (save modal structure different than expected)
- Replace existing file option - FAILING (save modal options different)
- Save with modifications - FAILING (save confirmation flow different)
- Handle save error - FAILING (error handling flow different)
- NOTE: Feature works manually per user - tests need modal selector refinement
-
Embedded Mode (4/6 tests - 67%) -
tests/e2e/dataverse/embedded-mode.spec.ts- Detect embedded mode from URL parameters
- Hide file loading buttons in embedded mode
- Show save button in embedded mode
- Pre-fill filename in save modal - FAILING (save modal doesn't exist or different structure)
- Maintain Dataverse context during edits
- Handle missing fileId parameter - FAILING (error handling incomplete)
-
Error Handling (2/4 tests - 50%) -
tests/e2e/dataverse/error-handling.spec.ts- Handle invalid JSON-LD file - FAILING (missing test fixture: invalid-syntax.json)
- Handle network error when loading shapes - FAILING (shape loading functionality different than expected)
- Handle validation errors gracefully
- Handle export with no data loaded
-
Cross-Browser (2/5 tests - 40%) -
tests/e2e/cross-browser/compatibility.spec.ts- Load and display file correctly
- Edit mode functions correctly - FAILING (changed class not applied or timing issue)
- Search functionality works
- Export functionality works - FAILING (export timing or download handling issue)
- Collapse/expand nodes works - FAILING (collapsed class not applied correctly)
-
Responsive (0/4 tests - 0%) -
tests/e2e/cross-browser/responsive.spec.ts- Mobile view (375px) - Layout adapts correctly - FAILING (horizontal overflow or layout issues)
- Tablet view (768px) - All controls accessible - FAILING (layout or control visibility issues)
- Desktop view (1920px) - Full layout displayed - FAILING (layout or sizing issues)
- Touch interactions work on mobile - FAILING (tap events or collapse functionality issues)
Total Progress: 137 tests created, 76 passing (55%)
Test Suites Summary:
- ✅ File Loading: 7/7 (100%)
- 🟡 Validation: 5/6 (83%)
- 🟡 Editing: 8/9 (89%) - "Changed" marking bug identified
- 🟢 Search: 8/8 (100%) - ✅ Search works perfectly!
- 🟡 Namespace Management: 6/9 (67%)
- 🟡 Array Operations: 5/9 (56%)
- 🟡 Export: 5/8 (63%)
- 🟡 Custom Namespace Properties: 7/12 (58%) - UI fixed, property addition still broken
- 🟡 Custom Property UI: 7/13 (54%) - ✅ 5 BUGS FIXED (popup UI replaced with inline)
- 🔴 Document Creation: 0/8 (0%) - Tests identify missing "Add Root Node" functionality
- 🟡 Dataverse Load: 2/5 (40%)
- 🟡 Dataverse Save: 1/5 (20%)
- 🟡 Integrated Mode: 4/6 (67%)
- 🟡 Error Handling: 2/4 (50%)
- 🟡 Cross-Browser: 2/5 (40%)
- 🔴 Responsive: 0/4 (0%) - Tests identify responsive design issues
Bugs Fixed in This Session:
- ✅ "Add Custom Property" popup button removed - Replaced with inline UI in both
renderBlankNodeandrenderNode - ✅ Inline UI now shown for all nodes - Even nodes without SHACL suggestions get unified inline UI
- ✅ No more browser prompt() - Old popup implementation completely removed
- ✅ Consistent UI across all nodes - All nodes use the same unified add component
- ✅ UI matches screenshot 1 - Namespace selector + inline input + Add button
Key Issues Still Broken:
- "Changed" marking disappears on mode toggle: Data changes are preserved but visual highlighting (teal) is removed when switching between edit/view mode (affecting 1 test)
- Property addition not working: Clicking "Add" in inline UI doesn't add the property (affecting 7 tests)
- Search functionality: Highlights matches correctly
- Array operations: Display and conversion issues
- Export: User changes not being preserved
- Namespace validation: Prefix uniqueness validation not working
Next Steps:
- Fix property addition in inline UI
- Fix array operations and conversion features
- Ensure export preserves all user modifications
- Complete remaining test suites (Document Creation, Dataverse, Cross-Browser)
- Tests should fail when functionality is broken
- Tests should reflect real user behavior
- Never modify tests to pass - fix the code instead
- If a test fails, ask:
- Is the test wrong? (Check actual UI behavior manually)
- Is the code wrong? (Fix the bug)
- Is the requirement wrong? (Update documentation)
- ❌ BAD:
await expect(page.locator('.node-type')).toBeVisible()- Problem: Doesn't verify WHICH type is shown
- ✅ GOOD:
await expect(page.locator('[data-testid="node-type-0"]')).toHaveText('WideDataSet')- Verifies the exact type value
- Verify actual rendered content, not just presence
- Check counts, text, values, states
- Test interactions produce expected results
- Validate error messages are clear and actionable
// ❌ Insufficient
await expect(page.locator(".node-card")).toBeVisible();
// ✅ Comprehensive
await expect(page.locator(".node-card")).toHaveCount(26);
await expect(page.locator('[data-node-id="#Sample_Dataset"]')).toBeVisible();
await expect(page.locator('[data-testid="node-type-0"]')).toHaveText(
"WideDataSet"
);
await expect(page.locator('[data-testid="property-name"]')).toContainText(
"Sample Dataset"
);- Each test should be independent
- Tests should clean up after themselves
- Use fresh browser context for each test
- Don't rely on execution order
Problem: When multiple elements share the same data-testid, Playwright throws a "strict mode violation" error:
Error: locator('[data-testid="property-identifier"]') resolved to 2 elements
Root Cause: Test data may contain duplicate property names across different nodes. For example, both #dataset1 and #variable1 might have an "identifier" property, resulting in multiple elements with data-testid="property-identifier".
Solution: Make selectors node-specific by including the parent data-node-id attribute:
// ❌ BAD: Ambiguous selector (may match multiple elements)
const property = page.locator('[data-testid="property-identifier"]');
// ✅ GOOD: Node-specific selector (matches exactly one element)
const property = page.locator(
'[data-node-id="#dataset1"][data-testid="property-identifier"]'
);Pattern for Dynamic Tests:
- Find the parent context (e.g., property row or node card)
- Extract the
data-node-idattribute - Build a compound selector that includes both node ID and test ID
// Example: Delete property test
const deleteBtn = page
.locator('[data-testid="delete-property-btn-identifier"]')
.first();
const propertyRow = deleteBtn.locator(
'xpath=ancestor::div[contains(@class, "property-row")][1]'
);
const nodeId = await propertyRow.getAttribute("data-node-id");
const propertyKey = "identifier";
// Build node-specific selector
const selector = `[data-node-id="${nodeId}"][data-testid="property-${propertyKey}"]`;
const property = page.locator(selector);
// Now assertions are precise
await expect(property).toHaveClass(/deleted/);Key Takeaways:
- Always check for duplicate
data-testidvalues when debugging "strict mode violation" errors - Use XPath
ancestor::to navigate from a specific element to its parent container - Include parent context (like
data-node-id) in selectors to disambiguate - If manual testing shows the feature works but the test fails, suspect selector ambiguity
// Edit Mode Toggle
"#toggle-edit-btn"; // Text changes: "Enable Editing" → "View Mode"
"#toggle-edit-btn .glyphicon"; // Icon changes: glyphicon-edit → glyphicon-eye-open
// Save Button (Dataverse mode)
"#save-btn"; // Hidden initially, visible in edit mode
// Export Button
"#export-btn"; // Always visible
// Collapse/Expand All
"#collapse-all-btn"; // Collapses all node cards
"#expand-all-btn"; // Expands all node cards
// Validation Status
"#validation-status"; // Shows validation badge
"#validation-details"; // Shows detailed violations// Local File Input
"#local-file-input"; // File input (hidden)
"#load-local-btn"; // Triggers file input
// Dataverse Loading
"#load-dataverse-btn"; // Opens Dataverse modal
"#loadDataverseModal"; // Modal for loading from Dataverse
// Shape Selector
"#shape-selector"; // Dropdown for SHACL shapes
"#custom-shape-url"; // Input for custom shape URL (hidden by default)// Search Input
"#search-input"; // Main search box
"#clear-search-btn"; // Clear search (× button)
"#search-counter"; // Shows "X of Y matches"
// Search Controls
"#toggle-case-btn"; // Toggle case sensitivity
"#toggle-regex-btn"; // Toggle regex mode
"#prev-match-btn"; // Previous match
"#next-match-btn"; // Next match
// Search Scope
'input[name="search-scope"]'; // Radio buttons (all/keys/values/types)
// Filter Panel
"#toggle-filter-panel"; // Toggle filter panel visibility
"#validation-filter"; // Dropdown: all/valid/invalid/modified/missing-required
'input[name="property-status"]'; // Radio: all/shacl-only/extra-only
"#clear-all-filters-btn"; // Clear all filters"#namespace-section"; // Namespace section container
"#namespace-content"; // Collapsible content
"#toggle-namespace-btn"; // Collapse/expand button
"#add-namespace-btn"; // Add namespace (edit mode only)
"#namespace-table-body"; // Table body with namespace rows
"#namespaceModal"; // Modal for add/edit namespace"#content"; // Main content container where nodes render
"#add-root-node-container"; // Add root node component (edit mode)// Node Card Container
".node-card"; // All node cards
'[data-node-id="<id>"]'; // Specific node by ID
'[data-testid="node-card-<sanitized-id>"]'; // Test-specific selector
// Node Header
".node-header"; // Clickable header for collapse/expand
'[data-testid="node-header-<sanitized-id>"]';
".collapse-icon"; // Chevron icon (down when expanded, right when collapsed)
// Node Identity
".node-id"; // Node @id value
'[data-testid="node-id-<sanitized-id>"]';
".node-type"; // Node @type value(s)
'[data-testid="node-type-0"]'; // First type
'[data-testid="node-type-1"]'; // Second type (if multiple)
// Node Body
".node-body"; // Contains all properties
'[data-testid="node-body"]';
".node-body.view-mode"; // View mode styling// Property Row Container
".property-row"; // All property rows
'[data-property="<key>"]'; // Specific property by key
'[data-testid="property-<sanitized-key>"]';
// Property Classification
".property-row.shacl-defined"; // SHACL-defined property
".property-row.extra-field"; // Extra field (not in shape)
".property-row.required"; // Required property
".property-row.changed"; // Modified in edit mode (teal highlight)
// Property Badge
".property-badge"; // Classification badge
'[data-testid="badge-<sanitized-key>"]';
".property-badge.required"; // "REQUIRED" badge (red)
".property-badge.shacl-defined"; // "SHACL" badge (blue)
".property-badge.extra"; // "EXTRA" badge (yellow)
// Property Labels
".property-label"; // Human-readable label
'[data-testid="property-label"]';
".property-path"; // Technical path/key
'[data-testid="property-path"]';
// Property Values
".property-value"; // Value container
'[data-testid="property-value"]';
".value-display"; // Text display (view mode)
'input[data-property="<key>"]'; // Input field (edit mode)
'select[data-property="<key>"]'; // Dropdown (edit mode)
".array-value"; // Array value wrapper
'[data-array-index="<n>"]'; // Array element by index// Property Actions
".delete-property-btn"; // Delete property (× button)
".convert-to-array-btn"; // Convert single value to array
".convert-to-single-btn"; // Convert array to single value
".add-value-btn"; // Add array element
".add-reference-btn"; // Add reference/object
".jump-to-node-btn"; // Jump to referenced node
// Add Property Section
".add-property-section"; // Add properties section (edit mode)
".item-dropdown"; // Property dropdown
".item-description"; // Property description area
".add-item-btn"; // Add selected property button
".custom-item-section"; // Custom property section".inline-node-card"; // Nested object displayed inline
".node-reference-link"; // Reference link with jump button// Node-level validation
".node-card.valid"; // Valid node
".node-card.invalid"; // Invalid node
".validation-icon"; // ✓ or ✗ icon
// Property-level validation
".property-row.invalid"; // Invalid property
".validation-error-message"; // Error message// Load from Dataverse
"#loadDataverseModal"; // Modal container
"#file-url-input"; // File URL input
"#api-token-input"; // API token input (optional)
// Save to Dataverse
"#saveModal"; // Modal container
"#save-as-new-radio"; // Save as new file radio
"#replace-existing-radio"; // Replace existing radio
// Add/Edit Namespace
"#namespaceModal"; // Modal container
"#namespace-prefix-input"; // Prefix input
"#namespace-uri-input"; // URI input
// Add Reference/Object
"#addReferenceModal"; // Modal container
"#existingNodeSelect"; // Existing node dropdown
"#newNodeType"; // New node type input
"#confirmAddReference"; // Confirm button".alert-success"; // Success message
".alert-danger"; // Error message
".alert-warning"; // Warning message
".alert-info"; // Info message- data-testid attributes (most stable)
- id attributes (stable if unique)
- data-* custom attributes
- CSS classes (less stable, can change with styling)
- Text content (least stable, changes with i18n)
// ✅ Good: Specific and stable
page.locator('[data-node-id="#Sample_Dataset"]');
page.locator('[data-testid="property-name"]');
// ✅ Good: Scoped within parent
page.locator(".node-card").filter({ hasText: "WideDataSet" });
page
.locator('[data-node-id="#Sample_Dataset"]')
.locator('[data-property="name"]');
// ⚠️ Use carefully: Text-based
page.locator('button:has-text("Enable Editing")');
// ❌ Avoid: Too broad
page.locator(".btn");
page.locator("input");For elements with dynamic IDs (like blank nodes), use:
// Sanitize ID for test selector
const sanitizedId = id.replace(/[^a-zA-Z0-9]/g, "_");
// Results in: #Sample_Dataset → _Sample_Dataset
// _:b0 → __b0This document outlines a comprehensive testing strategy for the CDI Viewer using Playwright for automated end-to-end testing. The tests cover both standalone mode (GitHub Pages) and Dataverse integration mode (previewer), ensuring all functionality works correctly in both contexts.
- Test Framework: Playwright
- Languages: TypeScript for type safety
- Mocking Strategy:
- Mock Service Worker (MSW) for Dataverse API endpoints
- Local test files in
examples/cdi/ - Mock SHACL shape responses
- Test Organization: Page Object Model pattern
- CI/CD: GitHub Actions integration
tests/
├── e2e/
│ ├── standalone/
│ │ ├── file-loading.spec.ts
│ │ ├── editing.spec.ts
│ │ ├── validation.spec.ts
│ │ ├── search-filter.spec.ts
│ │ ├── namespace-management.spec.ts
│ │ ├── document-creation.spec.ts
│ │ ├── array-operations.spec.ts
│ │ └── export.spec.ts
│ ├── dataverse/
│ │ ├── load-from-dataverse.spec.ts
│ │ ├── save-to-dataverse.spec.ts
│ │ ├── integrated-mode.spec.ts
│ │ └── error-handling.spec.ts
│ └── cross-browser/
│ ├── compatibility.spec.ts
│ └── responsive.spec.ts
├── fixtures/
│ ├── test-data/
│ │ ├── simple.jsonld
│ │ ├── complex-nested.jsonld
│ │ ├── schema-org-dataset.jsonld
│ │ ├── invalid-data.jsonld
│ │ └── minimal-ddi-cdi.jsonld
│ ├── shapes/
│ │ ├── test-shapes.ttl
│ │ └── minimal-shapes.ttl
│ └── mocks/
│ ├── dataverse-api-responses.ts
│ └── shape-responses.ts
├── page-objects/
│ ├── ViewerPage.ts
│ ├── ToolbarComponent.ts
│ ├── FilterPanelComponent.ts
│ ├── NodeCardComponent.ts
│ ├── PropertyRowComponent.ts
│ ├── NamespaceModalComponent.ts
│ └── SaveModalComponent.ts
├── helpers/
│ ├── dataverse-mock-server.ts
│ ├── test-data-generator.ts
│ └── assertions.ts
└── playwright.config.ts
Tests for the viewer running independently on GitHub Pages without Dataverse integration.
Setup:
- Navigate to
https://libis.github.io/cdi-viewer/ - Wait for page to fully load
- DDI-CDI shapes should auto-load
Actions:
- Click "Load Local File" button
- Upload
fixtures/test-data/simple.jsonld - Wait for rendering to complete
Expected Results:
- File loads successfully
- Success message appears: "Loaded: simple.jsonld"
- All nodes from
@graphare rendered as cards - Properties are visible and correctly classified
- Namespace section appears and shows context prefixes
- Filter panel remains collapsed
- Validation runs automatically
- Validation status badge appears
Assertions:
await expect(page.locator(".alert-success")).toContainText(
"Loaded: simple.jsonld"
);
await expect(page.locator(".node-card")).toHaveCount(expectedNodeCount);
await expect(page.locator("#namespace-section")).toBeVisible();
await expect(page.locator("#namespace-content")).toBeHidden(); // Collapsed by default
await expect(page.locator("#validation-status")).toBeVisible();Setup:
- Clean browser state
- Navigate to viewer
Actions:
- Load
fixtures/test-data/complex-nested.jsonld(contains 10+ nodes, nested objects, arrays) - Wait for complete rendering
Expected Results:
- All nodes render correctly
- Nested nodes show proper indentation
- References between nodes are clickable
- Jump buttons navigate to referenced nodes
- Collapse/expand works for all nodes
Assertions:
await expect(page.locator(".node-card")).toHaveCount(10, { timeout: 5000 });
const jumpButtons = page.locator('button:has-text("Jump to")');
await expect(jumpButtons).toHaveCount(expectedReferenceCount);
// Test reference navigation
await jumpButtons.first().click();
await expect(page.locator(".node-card.highlight")).toBeVisible();Setup:
- Navigate to viewer
Actions:
- Attempt to load
fixtures/test-data/no-context.jsonld
Expected Results:
- File loads but shows warning
- Namespace section not displayed
- Properties still render
- Generic JSON-LD mode activated
Setup:
- Navigate to viewer
Actions:
- Attempt to load
fixtures/test-data/invalid-syntax.json(invalid JSON)
Expected Results:
- Error alert appears
- Error message: "Failed to load file: Unexpected token..."
- Content area remains empty
- No crash or console errors
Setup:
- Navigate to
https://libis.github.io/cdi-viewer/?shacl=generic
Actions:
- Load
fixtures/test-data/schema-org-dataset.jsonld - Select "Custom URL" from shape selector
- Enter custom shape URL
- Load shapes
Expected Results:
- File loads without DDI-CDI shapes
- Can select different shape vocabularies
- Properties initially shown as EXTRA (yellow badges)
- After loading shapes, properties reclassified correctly
Setup:
- Load a test file
- Verify in view mode initially
Actions:
- Click "Enable Editing" button
- Wait for UI changes
Expected Results:
- Button changes to "Disable Editing" (warning style)
- Input fields become editable
- "Save to Dataverse" button appears
- Add property sections appear
- Add Root Node component appears at bottom
- "Add Namespace" button appears
- Validation runs automatically
Assertions:
await page.click("#toggle-edit-btn");
await expect(page.locator("#toggle-edit-btn")).toHaveClass(/btn-warning/);
await expect(page.locator("#save-btn")).toBeVisible();
await expect(page.locator("#add-root-node-container")).toBeVisible();
await expect(page.locator('input[type="text"]').first()).toBeEnabled();Setup:
- Load file and enable edit mode
Actions:
- Locate a text property (e.g.,
name) - Clear existing value
- Type new value: "Updated Name"
- Tab out or click elsewhere
Expected Results:
- Property row gains "changed" class (teal highlight)
- Original value stored as data attribute
- Validation updates after 3 seconds (debounced)
- Save button remains enabled
Assertions:
const nameInput = page.locator('input[data-property="name"]');
await nameInput.fill("Updated Name");
await expect(nameInput.closest(".property-row")).toHaveClass(/changed/);
// Wait for debounced validation
await page.waitForTimeout(3500);
await expect(page.locator("#validation-status")).toBeVisible();Setup:
- Load file with numeric property
- Enable edit mode
Actions:
- Find number input (e.g.,
xsd:integerproperty) - Enter invalid text: "abc"
- Enter valid number: "42"
Expected Results:
- Invalid text rejected by input type="number"
- Valid number accepted
- Property marked as changed
- Validation passes
Setup:
- Load file with date property
- Enable edit mode
Actions:
- Click date picker icon
- Select date from calendar widget
- Verify format
Expected Results:
- Date picker opens
- Selected date appears in ISO format (YYYY-MM-DD)
- Property marked as changed
Setup:
- Load file and enable edit mode
Actions:
- Click delete button (×) on optional property
- Confirm deletion if prompted
Expected Results:
- Property row removed from DOM
- Property removed from data structure
- Save button remains enabled
- Validation updates
Assertions:
const propertyRow = page.locator('.property-row:has-text("optional-field")');
await propertyRow.locator(".delete-property-btn").click();
await expect(propertyRow).not.toBeVisible();Setup:
- Load file with required properties (sh:minCount > 0)
- Enable edit mode
Actions:
- Locate required property (marked with *)
- Attempt to click delete button
Expected Results:
- Delete button not present OR disabled
- Alert appears: "Cannot delete required property"
- Property remains in UI
Setup:
- Load file with
sh:inconstraint - Enable edit mode
Actions:
- Click dropdown for enumeration property
- Select different value from list
- Verify change
Expected Results:
- Dropdown shows all valid values from
sh:in - Selected value updates
- No invalid values can be entered
- Property marked as changed
Setup:
- Load file and enable edit mode
- Scroll to "Add Properties" section
Actions:
- Click property dropdown
- Search for property (e.g., "description")
- Select from dropdown
- Click "Add Property" button
Expected Results:
- New property row appears in node
- Input field or appropriate widget rendered
- Property classified as SHACL-defined (blue badge if optional, red if required)
- Property removed from "Add Properties" dropdown
- Save button enabled
Assertions:
await page.locator(".add-property-dropdown").selectOption("description");
await page.click(".add-property-btn");
await expect(
page.locator('.property-row:has-text("description")')
).toBeVisible();
await expect(
page.locator('.add-property-dropdown option:has-text("description")')
).not.toBeVisible();Setup:
- Load file and enable edit mode
Actions:
- Click "Add Custom Property" in unified add component
- Select namespace from dropdown
- Enter property name: "customField"
- Press Enter or click Add
Expected Results:
- Custom property added with selected namespace prefix
- Property classified as EXTRA (yellow badge)
- Text input rendered by default
- Property editable immediately
Setup:
- Load shapes with
sh:maxCount = 1property - Enable edit mode
Actions:
- Add property that has maxCount = 1
- Verify it disappears from dropdown
- Attempt to add same property again
Expected Results:
- Property added successfully first time
- Property no longer available in dropdown
- Cannot add duplicate
Setup:
- Load file missing required properties
- Enable edit mode
Actions:
- Check "Add Properties" dropdown
- Locate required properties (marked with
⚠️ )
Expected Results:
- Required properties appear at top of dropdown
- Visual indicator (
⚠️ or red text) shows priority - Adding required property improves validation score
Setup:
- Load file and enable edit mode
- Find property with
sh:nodeorsh:classconstraint
Actions:
- Click "Add Reference/Object" button on property
- Modal opens
- Select "Create new object" radio
- Enter type name: "Person"
- Click "Add" button
Expected Results:
- Modal closes
- New blank node created with
@type: Person - Reference added to parent property:
{"@id": "_:b0"} - New node card renders inline/nested
- Node is immediately editable
Assertions:
await page.click('button:has-text("Add Reference/Object")');
await page.locator("#add-reference-modal").waitFor({ state: "visible" });
await page.click('input[value="create-new"]');
await page.fill("#new-node-type-input", "Person");
await page.click("#confirm-add-reference-btn");
await expect(page.locator('.node-card:has-text("Person")')).toBeVisible();Setup:
- Load file with multiple nodes
- Enable edit mode
Actions:
- Click "Add Reference/Object" on property
- Select "Reference existing node" radio
- Choose node from dropdown (shows all @id values)
- Click "Add"
Expected Results:
- Reference created:
{"@id": "selectedNodeId"} - Jump button appears in property value
- Clicking jump button scrolls to referenced node
- Referenced node highlights briefly
Setup:
- Create or load file with nested objects
- Enable edit mode
Actions:
- Expand parent node
- Locate nested child node
- Edit properties in nested node
- Save changes
Expected Results:
- Nested node properties editable
- Changes tracked independently
- Validation applies to nested structure
- Export includes nested changes
Setup:
- Load file with single-value property
- Enable edit mode
Actions:
- Locate property with single value
- Click "Convert to Array" button
- Confirm operation
Expected Results:
- Value becomes first array element
- "Add Value" button appears
- "Add Reference/Object" button appears (if applicable)
- "Convert to Single Value" button appears
- Array index shown: [0], [1], etc.
Assertions:
await page.click('button:has-text("Convert to Array")');
await expect(page.locator('.array-index:has-text("[0]")')).toBeVisible();
await expect(page.locator('button:has-text("Add Value")')).toBeVisible();Setup:
- Create array property
- Enable edit mode
Actions:
- Click "Add Value" button
- Enter value in new input field
- Click "Add Value" again
- Add multiple values
Expected Results:
- Each value gets array index: [0], [1], [2]...
- All values editable independently
- Delete button available for each value
- Can reorder values (if implemented)
Setup:
- Array property with multiple values
- Enable edit mode
Actions:
- Click delete button on array element
- Confirm if prompted
Expected Results:
- Value removed from array
- Array indices renumber automatically
- If last value removed, property remains as empty array or converts to single
Setup:
- Array property with multiple values
- Enable edit mode
Actions:
- Click "Convert to Single Value" button
- Confirm operation (warning: will lose all but first value)
Expected Results:
- Alert/confirm dialog appears
- After confirmation, only first value remains
- Array controls disappear
- "Convert to Array" button appears
Setup:
- Array property that accepts references
- Enable edit mode
Actions:
- Click "Add Reference/Object" on array property
- Choose to reference existing or create new
- Add reference
Expected Results:
- Reference added to array
- Jump button appears
- Can add multiple references to same array
- Mix of references and values possible (if allowed by schema)
Setup:
- Navigate to viewer with shapes preloaded
Actions:
- Load file with validation errors
Expected Results:
- Validation runs automatically after load
- Status badge appears within 3 seconds
- Invalid properties marked with red highlight
- Validation count shown: "X violations found"
Assertions:
await page.setInputFiles(
"#local-file-input",
"fixtures/test-data/invalid-data.jsonld"
);
await expect(page.locator("#validation-status")).toContainText(
"Validating...",
{ timeout: 1000 }
);
await expect(page.locator("#validation-status")).toContainText(
"violations found",
{ timeout: 4000 }
);
await expect(page.locator(".property-row.invalid")).toHaveCount(
expectedViolationCount
);Setup:
- Load valid file and enable edit mode
Actions:
- Edit a property value
- Wait 2 seconds (within debounce)
- Edit another property
- Wait 3+ seconds (debounce completes)
Expected Results:
- Validation doesn't trigger immediately on each keystroke
- Validation triggers 3 seconds after last edit
- Status shows "Validating..." then results
- Changes reflected in validation report
Assertions:
await nameInput.fill("New Value 1");
await page.waitForTimeout(2000);
await descInput.fill("New Value 2");
// Validation should not have run yet
await expect(page.locator("#validation-status")).not.toContainText(
"Validating..."
);
// Wait for debounce
await page.waitForTimeout(3500);
await expect(page.locator("#validation-status")).toContainText("Valid");Setup:
- Load file with 5+ validation errors
Actions:
- Trigger validation
- Expand violation details
- Review all violations
Expected Results:
- Status shows total count: "5 violations found"
- "Show Details" button appears
- Clicking button reveals list of violations
- Each violation shows:
- Node ID
- Property path
- Violation message
- Invalid properties highlighted in red throughout UI
Setup:
- File with validation errors loaded
- Edit mode enabled
Actions:
- Identify required field violation
- Add the required property
- Wait for auto-validation
- Verify error cleared
Expected Results:
- Adding required property triggers validation
- Violation count decreases
- Property no longer highlighted red
- When all fixed: "Data is valid" message
Setup:
- Load file
- Try different shape sources
Actions:
- Load DDI-CDI shapes → validate
- Switch to CDIF shapes → validate
- Load custom shape URL → validate
- Clear shapes (generic mode) → check validation status
Expected Results:
- Each shape source validates differently
- Appropriate properties classified for each vocabulary
- Generic mode shows "Select SHACL shapes to enable validation"
- No crashes when switching between shapes
Setup:
- Load file with long validation time (large file)
- Enable edit mode
Actions:
- Trigger validation (edit a property)
- Immediately trigger again (edit another property)
- Monitor validation status
Expected Results:
- First validation runs to completion
- Second validation queues/waits
- No parallel validation execution
- No race conditions or duplicate reports
Setup:
- Load file with multiple nodes
- File loaded in view mode
Actions:
- Type "dataset" in search box
- Verify matches highlight
Expected Results:
- Matching text highlighted in yellow
- Current match highlighted with pulse animation
- Counter shows "1 of X matches"
- Previous/Next buttons enabled
Assertions:
await page.fill("#search-input", "dataset");
await expect(page.locator(".search-match")).toHaveCount(expectedMatches);
await expect(page.locator("#search-counter")).toContainText(
"1 of " + expectedMatches
);
await expect(page.locator("#next-match-btn")).toBeEnabled();Setup:
- Active search with 5+ matches
Actions:
- Click "Next" button 3 times
- Click "Previous" button 2 times
- Use keyboard shortcuts: F3, Shift+F3
Expected Results:
- Counter updates: "2 of 5", "3 of 5", etc.
- Current match scrolls into view
- Current match has stronger highlight
- Wraps from last to first (and vice versa)
- Keyboard shortcuts work
Setup:
- File with mixed case content ("Dataset" and "dataset")
Actions:
- Search for "dataset"
- Note match count
- Click "Aa" button (enable case-sensitive)
- Compare match count
Expected Results:
- Case-insensitive finds both "Dataset" and "dataset"
- Case-sensitive finds only exact case matches
- Button shows active state when enabled
- Counter updates correctly
Setup:
- File loaded
Actions:
- Click ".*" button to enable regex
- Enter regex pattern:
\d{4}-\d{2}-\d{2}(dates) - Verify matches
Expected Results:
- Regex mode enabled (button active)
- Date strings highlighted
- Invalid regex shows error message
- Can disable regex mode
Assertions:
await page.click("#toggle-regex-btn");
await expect(page.locator("#toggle-regex-btn")).toHaveClass(/active/);
await page.fill("#search-input", "\\d{4}-\\d{2}-\\d{2}");
await expect(page.locator(".search-match")).toHaveCount(expectedDateCount);Setup:
- File loaded with diverse content
Actions:
- Uncheck "Values" checkbox
- Search for value that appears only in property values
- Verify no matches
- Re-check "Values" checkbox
- Verify matches appear
Expected Results:
- Unchecking scope excludes that content type
- Checking scope includes that content type
- Multiple scopes can be combined
- At least one scope must be checked
Setup:
- Active search with highlights
Actions:
- Click clear button (× in search box)
Expected Results:
- Search box cleared
- All highlights removed
- Counter hidden
- Navigation buttons disabled
- Clear button hidden
Setup:
- File with mix of valid and invalid nodes
- Enable edit mode (triggers validation)
Actions:
- Click "Advanced Filters" button
- Select "Invalid Only" from validation dropdown
- Observe results
Expected Results:
- Only invalid nodes visible
- Valid nodes hidden with
.hidden-by-filterclass - Parents of invalid nodes remain visible (bottom-up filtering)
- Filter badge shows "1 active filter"
Assertions:
await page.click("#toggle-filter-panel");
await page.selectOption("#validation-filter", "invalid");
await expect(page.locator(".node-card:not(.hidden-by-filter)")).toHaveCount(
expectedInvalidCount
);
await expect(page.locator("#active-filter-badge")).toContainText("1");Setup:
- File with SHACL shapes loaded
- Mix of SHACL-defined and extra properties
Actions:
- Open Advanced Filters
- Select "SHACL Only" radio button
- Verify only SHACL-defined properties visible
- Select "Extra Only"
- Verify only extra properties visible
Expected Results:
- SHACL Only: Yellow badge properties hidden
- Extra Only: Blue/red badge properties hidden
- Nodes with visible properties remain shown
- Filter count updates
Setup:
- Complex file with multiple node types
Actions:
- Apply validation filter: "Invalid Only"
- Apply property filter: "SHACL Only"
- Apply search: "identifier"
- Verify all filters work together
Expected Results:
- Only invalid nodes shown
- Only SHACL properties shown in those nodes
- Only properties matching "identifier" highlighted
- Bottom-up filtering ensures parent visibility
- All filters tracked in badge count
Setup:
- Multiple filters active
Actions:
- Click "Clear All Filters" button in filter panel
Expected Results:
- All filters reset to default ("All" options)
- All nodes and properties visible again
- Filter badge hidden
- Search remains active (independent)
Setup:
- Apply several filters
Actions:
- Set validation filter: "Valid Only"
- Set property filter: "SHACL Only"
- Refresh page (F5)
- Load same or different file
Expected Results:
- Filter settings restored from localStorage
- Same filters automatically applied to new data
- Filter panel state (open/closed) persisted
Setup:
- Load file with @context containing prefixes
Actions:
- Locate namespace section
- Expand if collapsed
- Review namespace table
Expected Results:
- Namespace section visible
- Table shows all prefixes and URIs from @context
- Built-in namespaces marked (e.g., no delete button)
- Custom namespaces have delete option
Assertions:
await page.click("#toggle-namespace-btn");
await expect(page.locator("#namespace-content")).toBeVisible();
await expect(page.locator("#namespace-table-body tr")).toHaveCount(
expectedPrefixCount
);Setup:
- Load file and enable edit mode
Actions:
- Click "Add Namespace" button
- Modal opens
- Enter prefix: "custom"
- Enter URI: "http://example.org/custom#"
- Click "Add Namespace"
Expected Results:
- Modal closes
- New row appears in namespace table
- Prefix "custom" available in property creation
- @context updated with new prefix
- Export includes new namespace
Assertions:
await page.click("#add-namespace-btn");
await expect(page.locator("#namespaceModal")).toBeVisible();
await page.fill("#namespacePrefixInput", "custom");
await page.fill("#namespaceUriInput", "http://example.org/custom#");
await page.click("#confirmNamespaceBtn");
await expect(
page.locator('#namespace-table-body tr:has-text("custom")')
).toBeVisible();Setup:
- Open add namespace modal
Actions:
- Attempt invalid prefix: "123invalid" (starts with number)
- Attempt invalid URI: "not-a-url"
- Attempt duplicate prefix
- Enter valid values
Expected Results:
- Invalid prefix rejected with error message
- Invalid URI rejected with error message
- Duplicate prefix rejected
- Confirm button disabled until valid
- Valid values accepted
Setup:
- File with custom namespace added
Actions:
- Click delete button next to custom namespace
- Confirm deletion
Expected Results:
- Namespace removed from table
- Namespace removed from @context
- Properties using that prefix remain but show full URI
- Built-in namespaces cannot be deleted
Setup:
- Custom namespace "myorg" added
Actions:
- Add custom property
- Select "myorg" from namespace dropdown
- Enter property name: "customField"
- Add property
Expected Results:
- Property added as "myorg:customField"
- Expands to full URI in exported JSON-LD
- Proper prefix resolution on export
Setup:
- Navigate to viewer (DDI-CDI mode)
- Do not load any file
Actions:
- Select SHACL shapes: "DDI-CDI Official"
- Click "Add Root Node" at bottom
- Select node type: "DataSet"
- Click Add
Expected Results:
- New empty document created
- DDI-CDI context automatically added
- Root node renders
- Enable edit mode automatically
- Default filename: "new-ddi-cdi-document.jsonld"
Setup:
- Navigate to viewer in generic mode (?shacl=generic)
Actions:
- Select SHACL shapes or create without shapes
- Add root node: "schema:Dataset"
- Begin adding properties
Expected Results:
- schema.org context added
- Appropriate namespace prefixes set up
- Can add schema.org properties
Setup:
- Empty document
Actions:
- Add first root node: "DataSet"
- Add properties and content
- Add second root node: "Agent"
- Add content to second node
Expected Results:
- Both nodes appear at root level
- @graph array contains both
- Nodes independent but can reference each other
- Export contains all root nodes
Setup:
- Generic mode, no shapes loaded
Actions:
- Add root node with custom type
- Add custom properties
- Export
Expected Results:
- Generic JSON-LD created
- All properties marked as EXTRA
- No validation (shapes not loaded)
- Clean @context with only used prefixes
Setup:
- Load file in view mode
- Do not make changes
Actions:
- Click "Export JSON-LD" button
Expected Results:
- File downloads immediately
- Filename matches original or uses default
- Content identical to loaded file
- @context preserved exactly
- Structure unchanged
Assertions:
const [download] = await Promise.all([
page.waitForEvent("download"),
page.click("#export-btn"),
]);
const content = await download.path();
// Verify content matches originalSetup:
- Load file and enable edit mode
- Make multiple changes:
- Edit 3 properties
- Add 2 properties
- Delete 1 property
- Add nested object
Actions:
- Click "Export JSON-LD"
Expected Results:
- All modifications included in export
- Deleted properties absent
- New properties present
- Nested objects included
- @context updated if namespaces added
- Valid JSON-LD structure
Setup:
- Add custom namespace
- Use it in properties
Actions:
- Export document
Expected Results:
- @context includes new namespace
- Properties use prefix notation
- Can re-import and edit
Setup:
- Create document from scratch
- Add content
Actions:
- Export
Expected Results:
- Clean JSON-LD structure
- Appropriate @context for selected vocabulary
- Default filename based on vocabulary
- Valid according to shapes (if loaded)
Setup:
- Load file with 50+ nodes
Actions:
- Make edits
- Export
Expected Results:
- Export completes without timeout
- File downloads successfully
- No data truncation
- Performance acceptable (<5 seconds)
Setup:
- File with multiple nodes loaded
Actions:
- Click node header to collapse
- Click header again to expand
Expected Results:
- Content area hides on collapse
- Chevron icon rotates
- Content shows on expand
- State independent for each node
Setup:
- File with 5+ nodes loaded
- Some collapsed, some expanded
Actions:
- Click "Collapse All" button
- Verify all collapsed
- Click "Expand All" button
- Verify all expanded
Expected Results:
- Collapse All: All nodes collapsed
- Expand All: All nodes expanded
- Nested nodes also affected
Assertions:
await page.click("#collapse-all-btn");
await expect(page.locator(".node-card.collapsed")).toHaveCount(totalNodeCount);
await page.click("#expand-all-btn");
await expect(page.locator(".node-card:not(.collapsed)")).toHaveCount(
totalNodeCount
);Setup:
- Load file with SHACL shapes
Actions:
- Hover over property label
- Wait for tooltip
Expected Results:
- Tooltip appears with sh:description text
- Tooltip dismisses on mouse leave
- Works for all properties with descriptions
Setup:
- Load viewer
Actions:
- Resize browser to mobile width (375px)
- Verify layout
- Resize to tablet (768px)
- Resize to desktop (1920px)
Expected Results:
- Mobile: Toolbar stacks vertically, buttons wrap
- Tablet: Reasonable layout, filters accessible
- Desktop: Full horizontal layout
- No horizontal scrolling
- All functions accessible at all sizes
Setup:
- Load file and enable edit mode
Actions:
- Tab through form fields
- Use Enter to submit forms
- Use Esc to close modals
- Use F3/Shift+F3 for search navigation
Expected Results:
- Tab order logical and complete
- All interactive elements reachable
- Enter submits where appropriate
- Escape closes modals
- Keyboard shortcuts work
Tests for the viewer running as a previewer within Dataverse, using mocked API responses.
Mock Implementation:
// tests/helpers/dataverse-mock-server.ts
import { rest } from "msw";
import { setupServer } from "msw/node";
const mockDataverseAPI = setupServer(
// Get file metadata
rest.get("*/api/files/:id", (req, res, ctx) => {
return res(
ctx.json({
data: {
id: req.params.id,
filename: "test-dataset.jsonld",
contentType: "application/ld+json",
datasetId: "doi:10.5072/FK2/TEST",
// ... full response
},
})
);
}),
// Download file content
rest.get("*/api/access/datafile/:id", (req, res, ctx) => {
return res(
ctx.json({
"@context": {
/* ... */
},
"@graph": [
/* ... */
],
})
);
}),
// Replace file
rest.post("*/api/files/:id/replace", (req, res, ctx) => {
return res(
ctx.json({
status: "OK",
data: { id: req.params.id },
})
);
}),
// Add file to dataset
rest.post("*/api/datasets/:id/add", (req, res, ctx) => {
return res(
ctx.json({
status: "OK",
data: { files: [{ id: "new-file-id" }] },
})
);
})
);
export { mockDataverseAPI };Setup:
- Start mock Dataverse server
- Mock responses for file ID 123
Actions:
- Navigate to
https://libis.github.io/cdi-viewer/?siteUrl=https://mock.dataverse.org&fileid=123 - Wait for file to load
Expected Results:
- File content fetched from API
- Data renders correctly
- Integrated mode detected (no manual load buttons)
- "Save to Dataverse" button visible
- Filename pre-filled in save modal
- URL field hidden in save modal
Assertions:
const mockSiteUrl = "https://mock.dataverse.org";
const fileId = "123";
await page.goto(`${viewerUrl}/?siteUrl=${mockSiteUrl}&fileid=${fileId}`);
await expect(page.locator("#load-local-btn")).not.toBeVisible();
await expect(page.locator("#load-dataverse-btn")).not.toBeVisible();
await expect(page.locator(".node-card")).toHaveCount(expectedCount);Setup:
- Mock Dataverse API
- Standalone viewer
Actions:
- Click "Load from Dataverse" button
- Modal opens
- Enter file URL:
https://mock.dataverse.org/file.xhtml?fileId=123 - Click "Load File"
Expected Results:
- URL validated in real-time
- File fetched from API
- Modal closes
- File renders
- Transitions to integrated mode
- Save button appears
Assertions:
await page.click("#load-dataverse-btn");
await page.fill(
"#loadDataverseUrlInput",
"https://mock.dataverse.org/file.xhtml?fileId=123"
);
await expect(page.locator("#confirmLoadBtn")).toBeEnabled();
await page.click("#confirmLoadBtn");
await expect(page.locator("#loadDataverseModal")).not.toBeVisible();
await expect(page.locator(".node-card")).toHaveCount(expectedCount);Setup:
- Mock API requiring authentication
Actions:
- Open Load from Dataverse modal
- Enter file URL
- Enter API token: "test-api-token-12345"
- Submit
Expected Results:
- API request includes token in header
- Unpublished file loads successfully
- Token not stored/logged
Assertions:
// Verify Authorization header in mock request
mockDataverseAPI.events.on("request:start", (req) => {
expect(req.headers.get("X-Dataverse-key")).toBe("test-api-token-12345");
});Setup:
- Open Load from Dataverse modal
Actions:
- Enter invalid URL: "not-a-url"
- Enter wrong domain: "https://other-site.com/file.xhtml?fileId=123"
- Enter valid URL
Expected Results:
- Invalid URL shows red error message
- Wrong format shows warning
- Valid URL shows green checkmark
- Confirm button disabled until valid
Setup:
- Mock server supports all formats
Actions: Test each format:
- JSF:
https://mock.dataverse.org/file.xhtml?fileId=123 - SPA:
https://mock.dataverse.org/spa/files?id=123 - API files:
https://mock.dataverse.org/api/files/123 - API access:
https://mock.dataverse.org/api/access/datafile/123 - Persistent ID:
https://mock.dataverse.org/api/access/datafile/:persistentId/?persistentId=doi:10.5072/FK2/123
Expected Results:
- All formats parsed correctly
- Correct API endpoint called
- File loads successfully in all cases
Setup:
- Mock API returns errors
Actions:
- Attempt load with 404 response
- Attempt load with 500 error
- Attempt load with network timeout
- Attempt load with malformed JSON
Expected Results:
- 404: "File not found" error message
- 500: "Server error" message
- Timeout: "Request timed out" message
- Malformed: "Invalid JSON-LD" error
- No crashes, user can retry
Setup:
- Load file via URL parameters (integrated mode)
- Enable edit mode and make changes
Actions:
- Click "Save to Dataverse"
- Modal shows pre-filled filename
- Enter API token
- Click confirm
Expected Results:
- Replace file API called with correct file ID
- Request includes modified JSON-LD content
- Success message appears
- Data marked as saved (no longer "changed")
Assertions:
mockDataverseAPI.use(
rest.post("*/api/files/:id/replace", async (req, res, ctx) => {
const body = await req.json();
expect(body["@graph"]).toBeDefined();
return res(ctx.json({ status: "OK" }));
})
);
await page.click("#save-btn");
await page.fill("#apiTokenInput", "test-token");
await page.click("#confirmSaveBtn");
await expect(page.locator(".alert-success")).toContainText("saved");Setup:
- Standalone mode
- Create new document from scratch
Actions:
- Click "Save to Dataverse"
- Enter dataset URL:
https://mock.dataverse.org/dataset.xhtml?persistentId=doi:10.5072/FK2/TEST - Enter filename: "new-metadata.jsonld"
- Enter API token
- Submit
Expected Results:
- Add file API called
- File uploaded to correct dataset
- Success confirmation
- File ID returned
Assertions:
mockDataverseAPI.use(
rest.post("*/api/datasets/:pid/add", async (req, res, ctx) => {
const formData = await req.formData();
expect(formData.get("file")).toBeDefined();
expect(formData.get("jsonData")).toContain("@graph");
return res(
ctx.json({
status: "OK",
data: { files: [{ id: "new-123" }] },
})
);
})
);Setup:
- File with validation violations
- Integrated mode
Actions:
- Attempt to save
- Validation check runs automatically
- Confirmation dialog appears
Expected Results:
- Warning: "Your data has validation errors. Save anyway?"
- Can cancel save
- Can proceed despite errors
- If proceed, saves with violations
Setup:
- Standalone mode with save to new dataset
Actions:
- Enter invalid dataset URL
- Enter valid URL
- Test different URL formats
Expected Results:
- Invalid URLs rejected with error message
- Valid dataset/file URLs accepted
- Real-time validation feedback
- Confirm button enabled/disabled appropriately
Setup:
- Mock various API errors
Actions:
- Save with 401 Unauthorized
- Save with 403 Forbidden
- Save with 500 Server Error
- Save with network failure
Expected Results:
- 401: "Invalid or missing API token"
- 403: "Permission denied"
- 500: "Server error occurred"
- Network: "Connection failed"
- Error alerts shown
- Can retry after error
Setup:
- File with 100+ nodes
- Make modifications
Actions:
- Save to Dataverse
Expected Results:
- Request completes successfully
- No timeout (within 30 seconds)
- Progress indicator shown
- Success confirmation
Setup:
- Navigate with URL parameters
Actions:
- Load
?siteUrl=X&fileid=Y
Expected Results:
- Integrated mode activated
- Load buttons hidden
- Save button visible even in view mode
- URL field hidden in save modal
- Filename pre-filled
Setup:
- Navigate without URL parameters
Actions:
- Load viewer normally
Expected Results:
- Standalone mode activated
- Load buttons visible
- Save button visible only after edit mode enabled
- URL field visible in save modal
Setup:
- Start in standalone mode
Actions:
- Use "Load from Dataverse" button
- Load file successfully
Expected Results:
- Mode transitions to integrated
- siteUrl and fileId stored
- Save behavior changes to integrated mode
- Can save back to Dataverse
Setup:
- Load in integrated mode
Actions:
- Make edits
- Refresh page (F5)
Expected Results:
- Returns to standalone mode (URL params lost)
- Data not persisted (expected behavior)
- User can re-load file
Setup:
- Run all core tests on Chromium
Expected Results:
- All features work
- No console errors
- Performance acceptable
Setup:
- Run critical path tests on Firefox
Expected Results:
- File loading works
- Editing works
- Validation works
- Export works
- Known issues documented
Setup:
- Run critical path tests on WebKit
Expected Results:
- Core functionality works
- Date picker differences handled
- Any polyfills working
Setup:
- Mobile viewport (375x667)
- Touch interactions
Expected Results:
- Responsive layout works
- Touch targets adequate
- Modals accessible
- Core features usable
Setup:
- Test on browsers 2-3 years old
Expected Results:
- Graceful degradation
- ES6 features supported or polyfilled
- Error messages if unsupported
Setup:
- Generate file with 200 nodes
Actions:
- Load file
- Measure render time
Expected Results:
- Loads within 10 seconds
- UI remains responsive
- No browser freezing
Setup:
- Large file loaded
Actions:
- Search for common term
- Measure highlight time
Expected Results:
- Results within 2 seconds
- Smooth scrolling
- No lag on navigation
Setup:
- Complex file with nested structures
Actions:
- Trigger validation
- Measure completion time
Expected Results:
- Completes within 5 seconds
- Progress indicator shown
- No UI blocking
Setup:
- Load, edit, save multiple times
Actions:
- Monitor memory usage
- Load/unload files repeatedly
Expected Results:
- No memory leaks
- Memory released on file unload
- Stable over 10+ cycles
Actions:
- Navigate entire UI with keyboard only
Expected Results:
- All functions accessible
- Focus indicators visible
- Logical tab order
Actions:
- Use NVDA/JAWS to navigate
Expected Results:
- All labels read correctly
- Button purposes announced
- Form fields labeled
- Error messages announced
Actions:
- Check all text/background combinations
Expected Results:
- WCAG AA compliance minimum
- Important info not color-only
- High contrast mode supported
Actions:
- Open/close modals
- Test focus trap
Expected Results:
- Focus moves to modal on open
- Focus trapped in modal
- Focus returns on close
P0 - Critical (Must Pass):
- File loading (standalone and integrated)
- Basic editing (text, numbers)
- Validation execution
- Export functionality
- Save to Dataverse (replace file)
P1 - High Priority:
- Array operations
- Complex object creation
- Search and filter
- Namespace management
- All error handling
P2 - Medium Priority:
- Advanced features
- Edge cases
- Performance tests
- Accessibility
P3 - Low Priority:
- UI polish
- Tooltips
- Animations
- Nice-to-have features
GitHub Actions Workflow:
name: E2E Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
browser: [chromium, firefox, webkit]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: npm ci
- run: npm run build
- run: npx playwright install
- run: npx playwright test --project=${{ matrix.browser }}
- uses: actions/upload-artifact@v3
if: failure()
with:
name: test-results-${{ matrix.browser }}
path: test-results/Run all tests:
npm run test:e2eRun specific suite:
npm run test:e2e -- standalone/editing.spec.tsDebug mode:
npm run test:e2e -- --debugHeaded mode (see browser):
npm run test:e2e -- --headed- HTML report generated automatically
- Screenshots on failure
- Video recording for failed tests
- Trace files for debugging
- Set up Playwright project
- Create page object models
- Implement Dataverse mock server
- Write 10 smoke tests (P0)
- File loading tests (standalone + integrated)
- Basic editing tests
- Validation tests
- Export tests
- Array operations
- Complex objects
- Search and filter
- Namespace management
- Dataverse integration tests
- Save functionality
- Error handling
- Cross-browser testing
- Performance tests
- Accessibility tests
- Edge cases
- Documentation
- Line Coverage: >80%
- Function Coverage: >90%
- Branch Coverage: >75%
- Flakiness: <2% test flakiness rate
- Execution Time: Full suite <10 minutes
- Failure Analysis: All failures categorized and tracked
- ✅ Chrome/Chromium: 100% pass rate
- ✅ Firefox: 95%+ pass rate
- ✅ Safari/WebKit: 90%+ pass rate
- ✅ Mobile: Core features working
- Identify feature to test
- Choose appropriate test suite file
- Follow existing patterns
- Use page objects
- Add assertions
- Update documentation
- Check test output/screenshots
- Run in headed mode
- Use trace viewer
- Check mock server logs
- Verify selector stability
- Review tests after UI changes
- Update selectors if needed
- Maintain page object models
- Keep mocks synchronized with API
This comprehensive testing plan ensures the CDI Viewer is thoroughly validated across all features, browsers, and use cases before release.