Testing EDS Blocks with Jupyter Notebooks - A Developer Playground

https://main--allaboutv2--ddttom.hlx.page/blogs/ddt/ai/media_184ca85230b191d1dc528167a71a1999962d9ceb9.png?width=2000&format=webply&optimize=medium
If you've ever found yourself refreshing your browser for the hundredth time just to see if your EDS block works with slightly different content, this one's for you.

Yes, really - Jupyter for JavaScript blocks

Jupyter notebooks aren't just for Python data scientists anymore. We've set up a complete testing environment that lets you experiment with Adobe Edge Delivery Services blocks interactively using JavaScript, right inside Visual Studio Code. Think of it as a playground where you can test, adjust, and iterate without the usual ceremony of spinning up dev servers and clearing caches.

You know the traditional workflow. Make a change, refresh the browser, wait for the page to load, realise you need different content, edit the document in Google Docs, wait for sync, refresh again. Repeat 47 times. There has to be a better way.

The notebook approach changes everything. Write your test content right in the cell, run it, see results immediately. Try different content structures, run again. Done. No servers, no syncing, no waiting - just pure feedback.

What makes this genuinely useful

The instant gratification hits differently. Type some HTML, run the cell, and boom - you see what your block does with it. No deployment, no build steps, just results. Every test shows you the before and after, so you watch your block work its magic on raw HTML and see exactly what emerges on the other side.

Want to see what happens if you nest 10 divs instead of 2? Try it. Want to test with empty content? Go ahead. This is a sandbox - play around without consequences. Markdown cells let you explain why you're testing something, which means future you (and your teammates) will actually understand your thought process.

Nothing you do in the notebook affects your actual codebase until you're ready. That makes it perfect for trying wild ideas.

The technical foundation

We're using tslab (a modern JavaScript kernel for Jupyter) combined with jsdom (a JavaScript implementation of the DOM). This combination works remarkably well for several reasons.

Traditional solutions like IJavascript require zeromq, which often breaks during installation with native compilation errors. tslab skips all that drama - no native dependencies, works with current Node.js versions, gets regular updates, and even supports TypeScript out of the box if you want it.

jsdom brings the browser environment to Node.js. It's like those flight simulators - not quite the real thing, but close enough for practice. You get document, window, and all the DOM APIs. CSS-related functions like getComputedStyle() work. Custom elements have support (with some limitations). Event handling functions as expected.

How the magic actually works

When you run a cell, jsdom creates a simulated browser complete with document, window, and all the DOM APIs. Your block gets loaded as a dynamic ES module import. CSS files inject into the virtual DOM - both global EDS styles and block-specific ones. The block runs its decoration function, transforming HTML just like it would in production. You see the results either in the notebook or saved as a styled HTML file.

Here's the core setup that makes everything work:

// Initialize jsdom virtual DOM

const { JSDOM } = require('jsdom');

const dom = new JSDOM('<!DOCTYPE html><html><head></head><body></body></html>', {

url: 'http://localhost',

pretendToBeVisual: true // Enables visual APIs like getComputedStyle()

});

// Expose DOM globals so blocks can use them

global.document = dom.window.document;

global.window = dom.window;

global.HTMLElement = dom.window.HTMLElement;

global.Element = dom.window.Element;

global.Node = dom.window.Node;

global.customElements = dom.window.customElements;

global.CustomEvent = dom.window.CustomEvent;

global.Event = dom.window.Event;

The CSS linking innovation

Here's where things get clever. Instead of embedding CSS into generated HTML files, we link to the actual source files:

<!-- Global EDS Styles -->

<link rel="stylesheet" href="../styles/styles.css">

<link rel="stylesheet" href="../styles/fonts.css">

<link rel="stylesheet" href="../styles/lazy-styles.css">

<!-- Block-specific styles -->

<link rel="stylesheet" href="../blocks/accordion/accordion.css">

This approach creates a brilliant workflow. Modify CSS, refresh browser, see changes instantly. No regeneration needed. The files stay compact - 2KB instead of 100KB. The structure matches how actual EDS sites load styles. Browser caching works between previews. You can inspect and modify the actual source files.

Your development rhythm becomes beautifully simple. CSS changes just need a browser refresh. JavaScript changes need a cell rerun. Content changes need a cell rerun. Everything happens fast.

The honest limitations

This isn't a real browser, so some things won't work perfectly. Interactive features like button clicks and form submissions won't work in the notebook output itself - but you can save the HTML and open it in a real browser. Web Components kind of work, but they're not fully supported. Stick with vanilla EDS blocks for best results. If jsdom doesn't support something, neither do we - but it covers most common use cases.

The notebook shows you raw HTML output. Want to see it styled? Use the saveBlockHTML() helper to generate a preview file.

Getting set up takes five minutes

The installation looks longer than it actually is. Think of it as assembling IKEA furniture - follow the instructions and you'll be fine.

Start with Node.js dependencies:

npm install jsdom

That's it for JavaScript. One package. Then handle Python dependencies:

# Get Jupyter (you probably already have this)

pip3 install jupyter

# Get tslab (the JavaScript kernel)

npm install -g tslab

# Tell Jupyter about tslab

tslab install --python=python3

# Install VS Code Jupyter extension

code --install-extension ms-toolsai.jupyter

Now follow these steps. Install Node.js if needed:

# On macOS with Homebrew

brew install node

# Verify

node -v

Install your project dependencies in the root folder:

npm install

Get Jupyter if you don't have it:

pip3 install jupyter

# Or if you're a conda person: conda install jupyter

Install tslab globally and register it:

npm install -g tslab

tslab install --python=python3

Install the VS Code Jupyter extension:

code --install-extension ms-toolsai.jupyter

Make sure it worked:

jupyter kernelspec list

You should see jslab in the list. Example output:

Available kernels:

jslab /Users/you/Library/Jupyter/kernels/jslab

python3 /Users/you/Library/Jupyter/kernels/python3

Open VS Code in your project root:

code .

Open test.ipynb from the file explorer. VS Code recognises the .ipynb extension and opens it in notebook mode. If prompted to select a kernel, choose "jslab".

Run the first cell - the magic setup cell. This initialises the DOM environment and sets up helper functions. You only do this once per session. Click the play button next to the cell or press Shift+Enter. Console output shows:

✓ DOM environment initialized

✓ Web Components (customElements) enabled

✓ Output directory ready: ./ipynb-tests

✓ Will link CSS files from /styles and /blocks folders

✓ loadBlockStyles() helper ready

✓ testBlock() helper ready

✓ saveBlockHTML() helper ready

========================================

Setup complete! Ready to test EDS blocks

========================================

Done. Ready to test blocks.

When things go wrong

JSLab not showing up as a kernel option in VS Code? Reinstall and register:

npm install -g tslab

tslab install --python=python3

jupyter kernelspec list # Check again

Restart VS Code after installing to ensure it picks up the new kernel.

Can't find modules? Make sure you opened VS Code from your project root. The notebook needs to see your blocks folder:

# Open VS Code from project root

cd /path/to/project/

code .

TypeScript complaining about document? Use global.document and global.window in your code. TypeScript gets confused because we're in Node.js land, not browser land. Prefix with global and you're sorted.

"Cannot find name 'document'" errors are TypeScript compile-time warnings. The code works fine at runtime because we're setting global.document. Ignore these errors or use global.document explicitly.

VS Code not recognising the notebook? Make sure the Jupyter extension is installed and enabled. Check the Extensions panel (Cmd+Shift+X or Ctrl+Shift+X) and search for "Jupyter".

The complete implementation

Let's examine the helper functions that make everything work.

testBlock() does most of the heavy lifting

async function testBlock(blockName, innerHTML = '') {

console.log(`\n=== Testing: ${blockName} ===`);

try {

// Load the block module dynamically

const path = await import('path');

const modulePath = path.resolve(`./blocks/${blockName}/${blockName}.js`);

const module = await import(modulePath);

const decorate = module.default;

// Load the block's CSS (for internal DOM testing)

const css = await loadBlockStyles(blockName);

// Create the block element

const block = global.document.createElement('div');

block.className = blockName;

// Add content if provided

if (innerHTML) {

block.innerHTML = innerHTML;

}

console.log('Before:', block.innerHTML || '(empty)');

// Decorate the block (this is where the magic happens)

await decorate(block);

const after = block.innerHTML || '';

console.log('After:', after.substring(0, 100) + (after.length > 100 ? '...' : ''));

return block;

} catch (error) {

console.error(`✗ Error testing ${blockName}:`, error.message);

console.error(` Block: ${blockName} may require browser-specific features`);

throw error;

}

}

This function dynamically imports your block's JavaScript module, loads the block's CSS file, creates a DOM element with the block's class, runs the decoration function, shows before and after transformation, and returns the decorated element.

Use it for quick tests. This is your go-to function most of the time.

saveBlockHTML() generates styled previews

async function saveBlockHTML(blockName, innerHTML = '', filename = null) {

const block = await testBlock(blockName, innerHTML);

// Check if block has a CSS file

const blockHasCSS = await hasBlockCSS(blockName);

// Build the complete HTML document

const html = `<!DOCTYPE html>

<html>

<head>

<meta charset="UTF-8">

<meta name="viewport" content="width=device-width, initial-scale=1.0">

<title>${blockName} Block Test</title>

<!-- Global EDS Styles (linked, not embedded!) -->

<link rel="stylesheet" href="../styles/styles.css">

<link rel="stylesheet" href="../styles/fonts.css">

<link rel="stylesheet" href="../styles/lazy-styles.css">

<!-- Block-specific styles (linked, not embedded!) -->

${blockHasCSS ? `<link rel="stylesheet" href="../blocks/${blockName}/${blockName}.css">` : '<!-- No block CSS file -->'}

<!-- Preview container styles (only thing embedded) -->

<style>

body {

display: block !important;

padding: 20px;

background: #f5f5f5;

}

.preview-container {

max-width: 1200px;

margin: 0 auto;

background: white;

padding: 40px;

border-radius: 8px;

box-shadow: 0 2px 8px rgba(0,0,0,0.1);

}

.preview-header {

border-bottom: 2px solid var(--light-color, #eee);

padding-bottom: 20px;

margin-bottom: 40px;

}

.preview-header h1 {

margin: 0;

color: var(--text-color, black);

}

</style>

</head>

<body class="appear">

<div class="preview-container">

<div class="preview-header">

<h1>${blockName} Block Preview</h1>

</div>

<main>

<div class="section">

<div>

${block.outerHTML}

</div>

</div>

</main>

</div>

</body>

</html>`;

// Save to ipynb-tests directory

const fs = await import('fs/promises');

const path = await import('path');

const outputFile = filename || `${blockName}-preview.html`;

const outputPath = path.resolve('./ipynb-tests', outputFile);

await fs.writeFile(outputPath, html, 'utf-8');

console.log(`\n✓ Saved HTML preview to: ipynb-tests/${outputFile}`);

console.log(` Open this file in your browser to see the styled result!`);

return outputPath;

}

This function tests the block (calls testBlock() internally), creates a complete HTML document with proper EDS structure, links to all global EDS stylesheets and the block's CSS file, wraps everything in a nice preview container, and saves to the ipynb-tests folder.

The implementation brilliance comes from linking all CSS instead of embedding it. Edit styles.css, refresh browser, see changes. Edit accordion.css, refresh browser, see changes. No need to regenerate the HTML file.

Use this when you need to see styling or test interactive features like clicks.

loadBlockStyles() manages CSS

async function loadBlockStyles(blockName) {

const fs = await import('fs/promises');

const path = await import('path');

const cssPath = path.resolve(`./blocks/${blockName}/${blockName}.css`);

try {

const css = await fs.readFile(cssPath, 'utf-8');

const style = global.document.createElement('style');

style.textContent = css;

global.document.head.appendChild(style);

console.log(`✓ Loaded styles for ${blockName}`);

return css;

} catch (e) {

console.log(`ℹ No CSS file found for ${blockName}`);

return null;

}

}

This reads the block's CSS file, injects it into the virtual DOM's head, and returns the CSS content or null if no file exists.

You rarely need to call this directly. The other helper functions handle it automatically. But it's available if you need granular control.

Real usage examples show the power

Testing a simple block takes one line:

// Test the HelloWorld block (no content needed)

const helloBlock = await global.testBlock('helloworld');

helloBlock.outerHTML

Output shows the transformation:

=== Testing: helloworld ===

✓ Loaded styles for helloworld

Before: (empty)

After: <div class="helloworld"><h1>Hello World!</h1><p>Welcome to EDS blocks.</p></div>

'<div class="helloworld"><h1>Hello World!</h1><p>Welcome to EDS blocks.</p></div>'

Testing blocks with content structure gets more interesting:

// Test Accordion block with Q&A content

const accordionContent = `

<div>

<div>What is EDS?</div>

<div>Edge Delivery Services is Adobe's modern web platform for building fast, performant websites.</div>

</div>

<div>

<div>How do blocks work?</div>

<div>Blocks are JavaScript functions that decorate DOM elements and transform content structure.</div>

</div>

<div>

<div>Why use notebooks?</div>

<div>Notebooks provide instant feedback for block development and testing.</div>

</div>

`;

const accordionBlock = await global.testBlock('accordion', accordionContent);

console.log('Created sections:', accordionBlock.querySelectorAll('details').length);

accordionBlock.outerHTML

Output demonstrates the transformation:

=== Testing: accordion ===

✓ Loaded styles for accordion

Before: <div>...</div> (original content)

After: <details>...</details><details>...</details><details>...</details>

Created sections: 3

'<details class="accordion-item">...</details>...'

Generating styled previews for browser viewing:

// Save accordion block as HTML file for browser viewing

const styledAccordionContent = `

<div>

<div>What is EDS?</div>

<div>Edge Delivery Services is Adobe's modern web platform.</div>

</div>

<div>

<div>How do blocks work?</div>

<div>Blocks transform DOM elements using JavaScript.</div>

</div>

`;

// Saves to ipynb-tests/accordion-preview.html

await global.saveBlockHTML('accordion', styledAccordionContent);

Output confirms the save:

=== Testing: accordion ===

✓ Loaded styles for accordion

Before: (content)

After: <details>...</details>

✓ Saved HTML preview to: ipynb-tests/accordion-preview.html

Open this file in your browser to see the styled result!

The generated HTML file links to all your actual CSS files, creating that beautiful live reload workflow.

Creating your own notebook in VS Code

You've got two paths here.

The easy way copies and customises:

cp test.ipynb my-custom-tests.ipynb

Open it in VS Code - just click the file in the file explorer. VS Code recognises .ipynb files and opens them in notebook mode. Delete examples you don't need, add your own. Done.

The "I want to build it myself" way starts with creating a new notebook. In VS Code, open the Command Palette (Cmd+Shift+P or Ctrl+Shift+P), type "Jupyter: Create New Blank Notebook", and select it. When prompted for a kernel, choose "jslab".

The first cell needs the setup magic. Copy the complete setup code from test.ipynb Cell 1. At minimum, you need:

const { JSDOM } = require('jsdom');

const dom = new JSDOM('<!DOCTYPE html><html><head></head><body></body></html>', {

url: 'http://localhost',

pretendToBeVisual: true

});

global.document = dom.window.document;

global.window = dom.window;

global.HTMLElement = dom.window.HTMLElement;

global.Element = dom.window.Element;

global.Node = dom.window.Node;

console.log('✓ Ready to test!');

For the full implementation with all helpers, copy the entire Cell 1 from test.ipynb.

Then add your test cells with normal JavaScript. Click the "+ Code" button to add new code cells:

// Load and test a block

const path = await import('path');

const module = await import(path.resolve('./blocks/myblock/myblock.js'));

const block = global.document.createElement('div');

block.className = 'myblock';

block.innerHTML = '<div>Your test content here</div>';

await module.default(block);

console.log(block.outerHTML);

That's really all there is to it. Save the notebook with Cmd+S or Ctrl+S, and give it a meaningful name.

Organising notebooks well matters

Think of your notebook like a story - it should flow naturally from simple to complex.

Start with a title and intro using a Markdown cell. Click "+ Markdown" or change a cell type to Markdown using the dropdown. Explain what you're testing and any important warnings.

The setup cell comes next. This code cell initialises jsdom and defines helper functions. Run this first, always.

Simple tests come next. Mix Markdown and Code cells. Ease into it with basic examples. Build confidence before getting fancy.

Then add your actual block tests using Code cells with Markdown explanations. Keep one block per section. Use markdown cells to explain what you're testing and why.

Visual output examples show how to save HTML files. These work great for blocks that need styling to make sense.

A reference section at the end provides quick reminders of helper functions and useful code snippets.

Writing tests that help future you

Good tests explain themselves. Compare these approaches:

// ✅ Yes! Future you will love this

// Testing accordion with 3 Q&A pairs

// Expected: Should create 3 <details> elements

const content = `

<div>

<div>What is EDS?</div>

<div>Edge Delivery Services...</div>

</div>

<div>

<div>How does it work?</div>

<div>It transforms content...</div>

</div>

<div>

<div>Why test in notebooks?</div>

<div>Instant feedback...</div>

</div>

`;

const block = await testBlock('accordion', content);

console.log('Created sections:', block.querySelectorAll('details').length);

// ❌ No! This is a puzzle, not a test

const x = '<div><div>Q</div><div>A</div></div>';

await testBlock('accordion', x);

Use Markdown cells liberally. Explain what you're testing and why. Your future self will thank you when you come back to this notebook in three months.

Example markdown cell:

## Testing Accordion Edge Cases

This section tests the accordion block with various edge cases:

1. Empty content (should handle gracefully)

2. Single item (should still create accordion)

3. Deeply nested content (should flatten appropriately)

Handle failures gracefully:

try {

const block = await testBlock('myblock', content);

console.log('✓ Works!', block.outerHTML.substring(0, 100));

} catch (error) {

console.error('✗ Nope:', error.message);

console.log('Might need a real browser for this one');

}

Save visual stuff for the browser:

// Can't really see styling in the notebook?

// Save it and open in a browser!

await saveBlockHTML('accordion', content, 'accordion-edge-case-test.html');

console.log('Check ipynb-tests/accordion-edge-case-test.html');

Helper functions quick reference

testBlock(blockName, innerHTML)

Loads your block, runs it, shows you what happened.

Parameters take a blockName string (matches folder name) and optional innerHTML string (HTML content to insert before decoration).

Returns the decorated block as a DOM element.

Example:

const block = await testBlock('accordion', '<div>your content</div>');

console.log(block.outerHTML);

Use this most of the time. This is your go-to for quick tests.

saveBlockHTML(blockName, innerHTML, filename?)

Tests your block and saves a nice HTML file you can open in a browser.

Parameters include blockName string, optional innerHTML string, and optional filename string (defaults to blockname-preview.html).

Returns a string path to the saved file.

The brilliant part is how the generated HTML files link to your actual CSS files (not embedded copies). You get real EDS styling from /styles/styles.css, /styles/fonts.css, and /styles/lazy-styles.css. Block-specific styles come from /blocks/blockname/blockname.css. The live reload capability means you modify CSS and just refresh the browser.

Examples:

// Saves as accordion-preview.html

await saveBlockHTML('accordion', content);

// Custom filename

await saveBlockHTML('accordion', content, 'my-special-test.html');

Use this when you need to see styling or test interactive features like clicks.

Everything saves to ipynb-tests folder with proper CSS links back to your source files.

loadBlockStyles(blockName)

Just loads the CSS for a block into the virtual DOM.

Takes a blockName string as parameter.

Returns a string (CSS content) or null.

Example:

const css = await loadBlockStyles('accordion');

console.log('CSS length:', css ? css.length : 'none');

You rarely need this. testBlock() and saveBlockHTML() already do this for you. But it's available if you need granular control.

Fitting this into your actual workflow

Here's how this works in real development:

# 1. You're working on a new block

code blocks/myblock/myblock.js

# 2. Open the notebook in VS Code (if not already open)

# Click test.ipynb in the file explorer

# 3. In the notebook, test your block

await testBlock('myblock', '<div>test content</div>');

# 4. Need to see it styled?

await saveBlockHTML('myblock', content);

# 5. Check it in a browser

open ipynb-tests/myblock-preview.html

# 6. Tweak the styles? No problem!

# Edit blocks/myblock/myblock.css in VS Code

# Just refresh the browser - CSS is linked, not embedded!

# 7. Adjust the JavaScript?

# Edit blocks/myblock/myblock.js in VS Code

# Rerun the cell in the notebook (Shift+Enter)

# 8. Happy with it?

git add blocks/myblock/

git commit -m "Add myblock with awesome features"

The workflow shines because CSS changes just need a browser refresh (files are linked), JS changes need a cell rerun (instant feedback), and content changes need a cell rerun. No build steps means no waiting. No servers means one less thing to manage.

VS Code makes this even smoother with split views. You can have your block code on the left, the notebook in the middle, and the terminal on the right. All your tools in one window.

When to use the notebook

Yes, use it for quick validation before committing, trying different content structures, debugging weird edge cases, exploring how blocks transform content, creating examples for documentation, rapid prototyping of new blocks, and testing blocks in isolation.

Maybe skip it for complex interactive features (just test in a real browser), animation testing (browser is better), cross-browser issues (obviously need real browsers), and full integration testing (use the real site).

Advanced tricks for when you're comfortable

Test multiple blocks at once with batch testing:

const blocks = ['accordion', 'tabs', 'cards', 'hero'];

for (const blockName of blocks) {

try {

await saveBlockHTML(blockName, getContentFor(blockName));

console.log(`✓ ${blockName} - looking good!`);

} catch (error) {

console.error(`✗ ${blockName} - nope: ${error.message}`);

}

}

Create custom helper functions tailored to your needs:

// Test the same block with different content variations

function testVariations(blockName, contentArray) {

return Promise.all(

contentArray.map((content, i) =>

saveBlockHTML(blockName, content, `${blockName}-variation-${i}.html`)

)

);

}

global.testVariations = testVariations;

// Now use it!

await testVariations('accordion', [

'<div><div>Test 1</div><div>Content 1</div></div>',

'<div><div>Test 2</div><div>Content 2</div></div>',

'<div><div>Test 3</div><div>Content 3</div></div>',

]);

Measure how long block decoration takes with performance checks:

console.time('accordion-decoration');

const block = await testBlock('accordion', content);

console.timeEnd('accordion-decoration');

Output shows timing:

accordion-decoration: 15.234ms

Analyse transformation details with before and after comparisons:

const block = global.document.createElement('div');

block.className = 'accordion';

block.innerHTML = content;

const before = block.innerHTML;

const beforeSize = new Blob([before]).size;

// Do the magic

await decorate(block);

const after = block.innerHTML;

const afterSize = new Blob([after]).size;

console.log('Original:', beforeSize, 'bytes');

console.log('Transformed:', afterSize, 'bytes');

console.log('Size ratio:', (afterSize / beforeSize).toFixed(2) + 'x');

console.log('Added:', (afterSize - beforeSize), 'bytes');

Generate test content programmatically with a content generator helper:

function generateAccordionContent(numItems) {

const items = Array.from({ length: numItems }, (_, i) => `

<div>

<div>Question ${i + 1}</div>

<div>This is answer ${i + 1} with some test content.</div>

</div>

`).join('');

return items;

}

// Test with different sizes

await saveBlockHTML('accordion', generateAccordionContent(3), 'accordion-3-items.html');

await saveBlockHTML('accordion', generateAccordionContent(10), 'accordion-10-items.html');

Save expected output for regression testing with snapshot testing:

async function createSnapshot(blockName, content, snapshotName) {

const block = await testBlock(blockName, content);

const html = block.outerHTML;

const fs = await import('fs/promises');

const path = await import('path');

const snapshotPath = path.resolve(`./ipynb-tests/snapshots/${snapshotName}.html`);

await fs.mkdir(path.dirname(snapshotPath), { recursive: true });

await fs.writeFile(snapshotPath, html, 'utf-8');

console.log(`✓ Snapshot saved: ${snapshotName}`);

return html;

}

// Create snapshots

await createSnapshot('accordion', testContent, 'accordion-baseline');

Project structure matters for paths

Here's where everything lives:

your-project/

├── test.ipynb # Main testing notebook

├── ipynb-tests/ # Generated HTML previews

│ ├── accordion-preview.html # Links to ../styles/*.css

│ ├── tabs-preview.html # Links to ../blocks/*/css

│ └── myblock-preview.html

├── styles/ # Global EDS styles

│ ├── styles.css # Core styles (linked in previews)

│ ├── fonts.css # Font declarations (linked)

│ └── lazy-styles.css # Lazy styles (linked)

├── blocks/ # Your EDS blocks

│ ├── accordion/

│ │ ├── accordion.js

│ │ └── accordion.css # Linked from preview HTML

│ ├── tabs/

│ │ ├── tabs.js

│ │ └── tabs.css

│ └── myblock/

│ ├── myblock.js

│ └── myblock.css

├── docs/

│ └── ipynb.md # This documentation

└── package.json # Dependencies (jsdom)

The structure matters because preview HTML files use relative paths (../styles/, ../blocks/) to link to your actual CSS files:

<!-- In ipynb-tests/accordion-preview.html -->

<link rel="stylesheet" href="../styles/styles.css">

<link rel="stylesheet" href="../blocks/accordion/accordion.css">

This means you can edit CSS files directly, just refresh the browser to see changes, no need to regenerate HTML files, and the structure matches real EDS sites.

Troubleshooting the tricky bits

JSLab isn't showing up as a kernel option in VS Code? Reinstall and register:

npm install -g tslab

tslab install --python=python3

jupyter kernelspec list # Should see jslab now

Restart VS Code after installing to ensure it picks up the new kernel. Use the Command Palette (Cmd+Shift+P or Ctrl+Shift+P) and select "Developer: Reload Window".

Still not showing? Try finding where tslab is installed and using the explicit path:

which tslab

# Should show: /usr/local/bin/tslab or /opt/homebrew/bin/tslab

# Try installing with explicit path

/usr/local/bin/tslab install --python=python3

Can't find your block? Make sure you opened VS Code from your project root. The notebook needs to see your blocks folder:

# Open VS Code from project root

cd /path/to/project/

code .

TypeScript complaining about document? This is expected. Use global.document and global.window in your code:

// ❌ Causes TypeScript compile-time error

const div = document.createElement('div');

// ✅ Works at runtime (ignore TypeScript warning)

const div = global.document.createElement('div');

The code works fine at runtime because we're setting up global.document in the setup cell. TypeScript just doesn't know about it at compile time.

VS Code not recognising the notebook? Make sure the Jupyter extension is installed and enabled. Check the Extensions panel (Cmd+Shift+X or Ctrl+Shift+X) and search for "Jupyter". The extension should be installed and enabled.

Kernel won't start in VS Code? Check the Output panel (View → Output) and select "Jupyter" from the dropdown to see kernel startup logs. Common issues include Node.js not in PATH or tslab installation problems.

Web Component blocks broken? Web Components have limited support in jsdom. Your options are to save and test in a real browser or check if customElements is available:

// Option 1: Save and test in real browser

await saveBlockHTML('myblock', content);

// Then open ipynb-tests/myblock-preview.html

// Option 2: Check if customElements is available

if (global.customElements) {

console.log('Custom elements supported');

} else {

console.log('Custom elements not available - skip this block');

}

Where are your styles? The notebook shows raw HTML output. Styling isn't visible there by design. For styled output, save as HTML and open in browser:

await saveBlockHTML('accordion', content);

// Open ipynb-tests/accordion-preview.html

The preview files link to your actual CSS, so edit the CSS and just refresh the browser - no regeneration needed.

Module not found errors? Check your paths. The notebook uses dynamic imports with absolute paths:

// This works - using path.resolve()

const path = await import('path');

const modulePath = path.resolve('./blocks/myblock/myblock.js');

const module = await import(modulePath);

// This might not work - relative path

const module = await import('../blocks/myblock/myblock.js'); // ❌

Import failed for ES modules? Make sure your package.json has:

{

"type": "module"

}

Or your block files use the .mjs extension.

Technical deep dive for the curious

Why use global.* instead of bare identifiers? TypeScript performs compile-time type checking and doesn't know about document in a Node.js context. Using global.document tells TypeScript to look at runtime globals, avoiding compile errors while maintaining runtime functionality:

// ✅ Works in notebook environment

global.document.createElement('div');

// ❌ Causes TypeScript compile-time errors

document.createElement('div');

The CSS linking strategy shows real innovation. Instead of embedding thousands of lines of CSS, we link to source files. Compare the old approach of embedding 5000+ lines of CSS in a style tag versus linking to actual files. This creates smaller HTML files (approximately 2KB instead of 100KB), enables live reload (edit CSS, refresh browser), matches real structure (production EDS sites), enables better debugging (inspect actual source files), and allows browser caching (CSS files cached between previews).

Path resolution magic ensures blocks load correctly:

const path = await import('path');

// Absolute path from project root

const modulePath = path.resolve(`./blocks/${blockName}/${blockName}.js`);

// Works regardless of where Jupyter runs from

const module = await import(modulePath);

This approach means blocks load correctly even if Jupyter's working directory changes.

jsdom configuration matters for functionality:

const dom = new JSDOM('<!DOCTYPE html>...', {

url: 'http://localhost', // Sets document.URL

pretendToBeVisual: true, // Enables getComputedStyle

runScripts: 'outside-only' // Security (default)

});

The pretendToBeVisual option enables window.getComputedStyle(), element.offsetWidth and layout properties, and visual-related APIs that many blocks need.

Sharing notebooks with your team

Give notebooks clear names. Good examples include form-validation-tests.ipynb and hero-block-experiments.ipynb. Confusing examples include test2.ipynb and notebook-copy.ipynb.

Start with a good intro using markdown:

# Form Validation Testing

This notebook tests all form validation blocks with various edge cases.

**Important:** Run Cell 1 first to initialize the environment.

**What's tested:**

- Email validation patterns

- Phone number formats

- Required field validation

- Custom error messages

Include the setup cell by copying Cell 1 from test.ipynb so your notebook works standalone.

Add explanatory markdown to explain what each test does and why it matters.

Test with a fresh kernel using the Command Palette: "Jupyter: Restart Kernel and Run All Cells". Make sure it works from a clean state.

Then commit and share:

git add form-validation-tests.ipynb

git commit -m "Add form validation test notebook"

git push

What makes this solution unique

Looking back at what we've built, several innovations stand out.

The modern, reliable stack using tslab instead of IJavascript means no native compilation headaches, works with current Node.js versions, gets actual maintenance and updates, and has clean, predictable installation.

Smart CSS handling through linking to actual CSS files instead of embedding enables live CSS reload workflow, keeps generated files small, matches real EDS architecture, and makes debugging trivial.

Real EDS structure in generated previews uses proper EDS HTML structure with all standard EDS stylesheets linked, so blocks render exactly as they would in production.

Developer-friendly helpers keep things simple (two to three parameters maximum), are self-documenting (clear names, console output), tolerate errors (graceful failures), and work together (use together or separately).

Seamless workflow integration means this isn't replacing your development workflow - it's augmenting it through quick validation before committing, rapid prototyping of new ideas, edge case testing without ceremony, and documentation that actually runs.

VS Code integration brings everything together in one environment. Your editor, notebook, terminal, and file explorer all live in the same window. No context switching, no separate browser tabs for Jupyter - just smooth, unified development.

The bigger picture

This Jupyter notebook environment isn't just a testing tool - it's a developer experience enhancement. It removes friction from the block development process and makes experimentation feel natural rather than tedious.

The key innovations - using tslab for stability, linking CSS for live reload, providing clean helper functions, and integrating with VS Code - create a smooth workflow that respects your time and mental energy.

Whether you're validating a quick fix, prototyping a new block, or debugging an edge case, you now have a fast, reliable way to see results without the ceremony of full development environments.

The best tools disappear into your workflow. They don't make you think about them - they just make your work better. That's what we've built here.

/fragments/ddt/proposition

Related Articles

Back to Top