I was really surprised that no one had an approach to share on how to have testable JavaScript in a Phoenix project.
I will share my approach:
- Created a pure TypeScript package at
assets/vendor/package-name, with strict set to true and with Vitest for the test runner. - I write all my TypeScript code for the LiveView Hooks with tests.
- I include each hook in
assets/js/app.js. - I assign the hook as usual with
phx-hookand if needed I configure it viadata-*attributes in theheextemplate.
The assets/vendor/canvas/package.json:
{
"type": "module",
"devDependencies": {
"@types/jsdom": "^27.0.0",
"@types/node": "^24.10.1",
"jsdom": "^27.2.0",
"typescript": "^5.9.3",
"vitest": "^4.0.8"
}
}
The assets/vendor/canvas/tsconfig.json:
{
"compilerOptions": {
"target": "ESNext",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"allowJs": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"types": ["vitest/globals", "node"],
"baseUrl": ".",
"paths": {
"@src/*": ["src/*"],
"@test/*": ["test/*"]
}
},
"include": ["src/**/*.ts", "test/**/*.ts"],
"exclude": ["node_modules", "dist"]
}
The assets/vendor/canvas/vitest.config.ts:
import { defineConfig } from "vitest/config";
import path from "path";
export default defineConfig({
test: {
environment: "jsdom",
},
resolve: {
alias: {
"@src": path.resolve(__dirname, "./src"),
"@test": path.resolve(__dirname, "./test"),
},
},
});






















