DEV Community

Cover image for Symfony Asset Mapper: How to Finally Test JavaScript Properly Without the Pain
Jozef Môstka
Jozef Môstka

Posted on

Symfony Asset Mapper: How to Finally Test JavaScript Properly Without the Pain

You know the drill. Symfony Asset Mapper is a great tool. You've gotten rid of Webpack, npm install, and complex build processes. Everything is fast, clean, and modern. And then comes the fateful question: "And how do we actually test this?"

Then comes the reality check. Asset Mapper works on the principle of importmap.php, which your Node.js (and thus most test runners) has no clue about. You try to run a test and you get: ERR_MODULE_NOT_FOUND.

Many people just wave it off and say that testing Asset Mapper is simply impossible, or you have to switch back to a complex frontend stack. But what if I told you there's an elegant solution that bridges both worlds?

The "Aha!" moment: Symlinks as a bridge

My idea was simple: Node.js expects libraries in the node_modules folder. Symfony has them in assets/vendor/ or in vendor/ (for Stimulus bundles). So why not force Node.js to see what Symfony sees, without having to duplicate anything or "hack" the imports?

The solution is a small PHP script that reads your import map and creates a symlink structure in node_modules. Node.js will think everything is installed, while in reality, it will be reading the same files your browser uses.

Step 1: The PHP script that does the magic

Here is a simplified version of our "linker" script. It uses the Symfony Filesystem component for safe file manipulation.

// bin/setup-js-tests.php
require_once __DIR__ . '/../vendor/autoload.php';

use Symfony\Component\Filesystem\Filesystem;

$projectRoot = dirname(__DIR__);
$importmap = require $projectRoot . '/importmap.php';
$fs = new Filesystem();
$nodeModules = $projectRoot . '/node_modules';

if (!$fs->exists($nodeModules)) $fs->mkdir($nodeModules);

foreach ($importmap as $name => $config) {
    $targetDir = $nodeModules . '/' . $name;
    $sourcePath = isset($config['path']) 
        ? $projectRoot . '/' . ltrim($config['path'], './')
        : $projectRoot . '/assets/vendor/' . $name;

    if (!$fs->exists($sourcePath)) continue;

    if (!$fs->exists(dirname($targetDir))) $fs->mkdir(dirname($targetDir));

    if (is_dir($sourcePath)) {
        $fs->symlink($sourcePath, $targetDir);
    } else {
        // If it's a single file, we turn it into a package with index.js
        if (!$fs->exists($targetDir)) $fs->mkdir($targetDir);
        $fs->symlink($sourcePath, $targetDir . '/index.js');
        $fs->dumpFile($targetDir . '/package.json', json_encode([
            'name' => $name, 'type' => 'module', 'main' => 'index.js'
        ]));
    }
}
echo "Imports for JS tests have been successfully linked!\n";
Enter fullscreen mode Exit fullscreen mode

Step 2: Node.js Configuration

Now we need to tell Node.js how to run the tests. We'll use a standard package.json, but with a small improvement: we'll use the pretest hook, which automatically runs our PHP script before every test.

{
  "name": "my-project",
  "private": true,
  "type": "module",
  "scripts": {
    "test": "node --test tests/js/*.test.mjs",
    "test:watch": "node --watch --test tests/js/*.test.mjs",
    "pretest": "php bin/setup-js-tests.php"
  }
}
Enter fullscreen mode Exit fullscreen mode

Methodology: Why this way?

For testing, we chose the native Node.js test runner (available since version 20). Why?

  1. Zero configuration: No need to install Jest, Vitest, or anything similar.
  2. Speed: It starts instantly.
  3. Reality-based: No complex import mocking. You are testing the exact same files that run in production.

Thanks to symlinks, we can write clean imports in our tests:

import assert from 'node:assert/strict';
import test from 'node:test';
import { myFunction } from '../assets/js/my-function.js';
import { Midi } from '@tonejs/midi'; // This now works!
Enter fullscreen mode Exit fullscreen mode

This approach allows us to follow TDD (Test Driven Development) on the frontend while maintaining the simplicity of Asset Mapper. If you add a new library via importmap:require, just run npm test and everything will be automatically re-linked.

Conclusion

Testing JavaScript in Symfony Asset Mapper is not impossible. All it takes is a small bridge in the form of a PHP script, and you can leverage the power of a modern Node.js environment without having to leave the comfort zone of your PHP framework.

Give it a try in your project and say goodbye to "missing module" errors. Your code (and your mental health) will thank you. ?

Top comments (0)