Skip to content

Commit 1cd6459

Browse files
Modernize JavaScript build and test infrastructure (#2854)
This changelist modernizes the JavaScript build and test infrastructure in MaterialX, including the following changes: - Migrate from CommonJS to native ES modules, adding the EXPORT_ES6 flag to the Emscripten build. - Replace the Karma test framework with Playwright for browser tests. - Adopt npm workspaces, consolidating dependency management under a single root package. - Replace third-party build utilities with a native Node.js script for test setup.
1 parent 00ede1e commit 1cd6459

21 files changed

Lines changed: 6369 additions & 10400 deletions

.github/workflows/main.yml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -411,17 +411,20 @@ jobs:
411411
- name: JavaScript CMake Build
412412
run: cmake --build javascript/build --target install --config Release --parallel 2
413413

414+
- name: Install JavaScript Dependencies
415+
run: |
416+
npm ci
417+
npx playwright install --with-deps chromium
418+
working-directory: javascript
419+
414420
- name: JavaScript Unit Tests
415421
run: |
416-
npm install
417422
npm run test
418423
npm run test:browser
419424
working-directory: javascript/MaterialXTest
420425

421426
- name: Build Web Viewer
422-
run: |
423-
npm install
424-
npm run build
427+
run: npm run build
425428
working-directory: javascript/MaterialXView
426429

427430
- name: Deploy Web Viewer

javascript/MaterialXTest/.babelrc

Lines changed: 0 additions & 10 deletions
This file was deleted.

javascript/MaterialXTest/browser/karma.conf.js

Lines changed: 0 additions & 47 deletions
This file was deleted.
Lines changed: 158 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -1,131 +1,183 @@
1-
// MaterialX is served through a script tag in the test setup.
1+
import { test, expect } from '@playwright/test';
2+
import fs from 'fs';
3+
import path from 'path';
4+
import { fileURLToPath } from 'url';
25

3-
function createStandardSurfaceMaterial(mx)
4-
{
5-
const doc = mx.createDocument();
6-
const ssName = 'SR_default';
7-
const ssNode = doc.addChildOfCategory('standard_surface', ssName);
8-
ssNode.setType('surfaceshader');
9-
const smNode = doc.addChildOfCategory('surfacematerial', 'Default');
10-
smNode.setType('material');
11-
const shaderElement = smNode.addInput('surfaceshader');
12-
shaderElement.setType('surfaceshader');
13-
shaderElement.setNodeName(ssName);
14-
expect(doc.validate()).to.be.true;
15-
// Release local wrappers
16-
shaderElement.delete();
17-
smNode.delete();
18-
ssNode.delete();
19-
return doc;
20-
}
6+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
7+
const testRoot = path.resolve(__dirname, '..');
8+
9+
const MIME_TYPES = {
10+
'.html': 'text/html',
11+
'.js': 'application/javascript',
12+
'.wasm': 'application/wasm',
13+
'.data': 'application/octet-stream'
14+
};
2115

22-
describe('Generate Shaders', function ()
16+
//
17+
// Route handler that serves files from the local test build.
18+
//
19+
async function routeHandler(route)
2320
{
24-
let mx;
25-
const canvas = document.createElement('canvas');
26-
const gl = canvas.getContext('webgl2');
21+
const url = new URL(route.request().url());
22+
23+
if (url.pathname === '/')
24+
{
25+
return route.fulfill({
26+
contentType: 'text/html',
27+
body: '<!DOCTYPE html><html><body></body></html>'
28+
});
29+
}
2730

28-
this.timeout(60000);
31+
//
32+
// The Emscripten file packager may fetch .data relative to the page or
33+
// relative to the module. Always resolve it to _build/ to handle both cases.
34+
//
35+
let filePath;
36+
if (path.basename(url.pathname) === 'JsMaterialXGenShader.data')
37+
{
38+
filePath = path.join(testRoot, '_build', 'JsMaterialXGenShader.data');
39+
}
40+
else
41+
{
42+
filePath = path.join(testRoot, url.pathname);
43+
}
2944

30-
before(async function ()
45+
filePath = path.resolve(filePath);
46+
if (!filePath.startsWith(testRoot + path.sep))
3147
{
32-
mx = await MaterialX();
48+
return route.fulfill({ status: 403 });
49+
}
50+
51+
if (!fs.existsSync(filePath) || fs.statSync(filePath).isDirectory())
52+
{
53+
return route.fulfill({ status: 404 });
54+
}
55+
56+
const ext = path.extname(filePath);
57+
return route.fulfill({
58+
body: fs.readFileSync(filePath),
59+
contentType: MIME_TYPES[ext] || 'application/octet-stream'
3360
});
61+
}
3462

35-
it('Compile Shaders', () =>
63+
test.describe('Generate Shaders', () =>
64+
{
65+
test('Compile Shaders', async ({ page }) =>
3666
{
37-
const doc = createStandardSurfaceMaterial(mx);
38-
39-
const generators = []
40-
if (typeof mx.EsslShaderGenerator != 'undefined')
41-
generators.push(mx.EsslShaderGenerator.create());
42-
if (typeof mx.GlslShaderGenerator != 'undefined')
43-
generators.push(mx.GlslShaderGenerator.create());
44-
if (typeof mx.MslShaderGenerator != 'undefined')
45-
generators.push(mx.MslShaderGenerator.create());
46-
if (typeof mx.OslShaderGenerator != 'undefined')
47-
generators.push(mx.OslShaderGenerator.create());
48-
if (typeof mx.VkShaderGenerator != 'undefined')
49-
generators.push(mx.VkShaderGenerator.create());
50-
if (typeof mx.WgslShaderGenerator != 'undefined')
51-
generators.push(mx.WgslShaderGenerator.create());
52-
if (typeof mx.MdlShaderGenerator != 'undefined')
53-
generators.push(mx.MdlShaderGenerator.create());
54-
if (typeof mx.SlangShaderGenerator != 'undefined')
55-
generators.push(mx.SlangShaderGenerator.create());
56-
57-
const elem = mx.findRenderableElement(doc);
58-
for (let gen of generators)
67+
page.on('console', (msg) =>
68+
{
69+
if (msg.type() === 'error')
70+
{
71+
console.error(msg.text());
72+
}
73+
else
74+
{
75+
console.log(msg.text());
76+
}
77+
});
78+
79+
await page.route('**/*', routeHandler);
80+
await page.goto('http://materialx-test/');
81+
82+
const { error, generators } = await page.evaluate(async () =>
5983
{
60-
console.log("Generating shader for " + gen.getTarget() + "...");
61-
62-
const genContext = new mx.GenContext(gen);
63-
const stdlib = mx.loadStandardLibraries(genContext);
64-
doc.importLibrary(stdlib);
65-
66-
try
84+
const { default: MaterialX } = await import('/_build/JsMaterialXGenShader.js');
85+
const mx = await MaterialX();
86+
87+
const doc = mx.createDocument();
88+
const ssName = 'SR_default';
89+
const ssNode = doc.addChildOfCategory('standard_surface', ssName);
90+
ssNode.setType('surfaceshader');
91+
const smNode = doc.addChildOfCategory('surfacematerial', 'Default');
92+
smNode.setType('material');
93+
const shaderInput = smNode.addInput('surfaceshader');
94+
shaderInput.setType('surfaceshader');
95+
shaderInput.setNodeName(ssName);
96+
97+
const valid = doc.validate();
98+
shaderInput.delete();
99+
smNode.delete();
100+
ssNode.delete();
101+
if (!valid) return { error: 'Document validation failed', generators: [] };
102+
103+
const canvas = document.createElement('canvas');
104+
const gl = canvas.getContext('webgl2');
105+
106+
const generatorNames = [
107+
'EsslShaderGenerator', 'GlslShaderGenerator', 'MslShaderGenerator',
108+
'OslShaderGenerator', 'VkShaderGenerator', 'WgslShaderGenerator',
109+
'MdlShaderGenerator', 'SlangShaderGenerator'
110+
];
111+
112+
const elem = mx.findRenderableElement(doc);
113+
const generators = [];
114+
115+
for (const name of generatorNames)
67116
{
68-
const mxShader = gen.generate(elem.getNamePath(), elem, genContext);
117+
if (typeof mx[name] === 'undefined') continue;
118+
const gen = mx[name].create();
119+
const target = gen.getTarget();
120+
console.log('Generating shader for ' + target + '...');
69121

70-
const fShader = mxShader.getSourceCode("pixel");
122+
const genContext = new mx.GenContext(gen);
123+
const stdlib = mx.loadStandardLibraries(genContext);
124+
doc.importLibrary(stdlib);
71125

72-
if (gen.getTarget() == 'essl')
126+
try
73127
{
74-
const vShader = mxShader.getSourceCode("vertex");
128+
const mxShader = gen.generate(elem.getNamePath(), elem, genContext);
129+
const fShader = mxShader.getSourceCode('pixel');
130+
const errors = [];
75131

76-
const glVertexShader = gl.createShader(gl.VERTEX_SHADER);
77-
gl.shaderSource(glVertexShader, vShader);
78-
gl.compileShader(glVertexShader);
79-
if (!gl.getShaderParameter(glVertexShader, gl.COMPILE_STATUS))
132+
if (target === 'essl')
80133
{
81-
console.error("-------- VERTEX SHADER FAILED TO COMPILE: ----------------");
82-
console.error("--- VERTEX SHADER LOG ---");
83-
console.error(gl.getShaderInfoLog(glVertexShader));
84-
console.error("--- VERTEX SHADER START ---");
85-
console.error(fShader);
86-
console.error("--- VERTEX SHADER END ---");
87-
}
88-
expect(gl.getShaderParameter(glVertexShader, gl.COMPILE_STATUS)).to.equal(true);
134+
const vShader = mxShader.getSourceCode('vertex');
89135

90-
const glPixelShader = gl.createShader(gl.FRAGMENT_SHADER);
91-
gl.shaderSource(glPixelShader, fShader);
92-
gl.compileShader(glPixelShader);
93-
if (!gl.getShaderParameter(glPixelShader, gl.COMPILE_STATUS))
94-
{
95-
console.error("-------- PIXEL SHADER FAILED TO COMPILE: ----------------");
96-
console.error("--- PIXEL SHADER LOG ---");
97-
console.error(gl.getShaderInfoLog(glPixelShader));
98-
console.error("--- PIXEL SHADER START ---");
99-
console.error(fShader);
100-
console.error("--- PIXEL SHADER END ---");
136+
const glVS = gl.createShader(gl.VERTEX_SHADER);
137+
gl.shaderSource(glVS, vShader);
138+
gl.compileShader(glVS);
139+
if (!gl.getShaderParameter(glVS, gl.COMPILE_STATUS))
140+
{
141+
errors.push('Vertex shader: ' + gl.getShaderInfoLog(glVS));
142+
}
143+
144+
const glFS = gl.createShader(gl.FRAGMENT_SHADER);
145+
gl.shaderSource(glFS, fShader);
146+
gl.compileShader(glFS);
147+
if (!gl.getShaderParameter(glFS, gl.COMPILE_STATUS))
148+
{
149+
errors.push('Fragment shader: ' + gl.getShaderInfoLog(glFS));
150+
}
151+
152+
gl.deleteShader(glVS);
153+
gl.deleteShader(glFS);
101154
}
102-
expect(gl.getShaderParameter(glPixelShader, gl.COMPILE_STATUS)).to.equal(true);
103-
// Cleanup GL shaders
104-
gl.deleteShader(glVertexShader);
105-
gl.deleteShader(glPixelShader);
106-
}
107-
// Cleanup shader wrapper
108-
mxShader.delete();
109-
}
110-
catch (errPtr)
111-
{
112-
console.error("-------- Failed code generation: ----------------");
113-
if (typeof mx.getExceptionMessage === 'function')
114-
{
115-
console.error(mx.getExceptionMessage(errPtr));
155+
156+
generators.push({ target, ok: errors.length === 0, errors });
157+
mxShader.delete();
116158
}
117-
else
159+
catch (errPtr)
118160
{
119-
console.error(errPtr);
161+
const msg = typeof mx.getExceptionMessage === 'function'
162+
? mx.getExceptionMessage(errPtr) : String(errPtr);
163+
generators.push({ target, ok: false, errors: [msg] });
120164
}
165+
166+
stdlib.delete();
167+
genContext.delete();
168+
gen.delete();
121169
}
122-
// Cleanup per-generator wrappers
123-
stdlib.delete();
124-
genContext.delete();
125-
gen.delete();
170+
171+
elem.delete();
172+
doc.delete();
173+
return { generators };
174+
});
175+
176+
expect(error).toBeUndefined();
177+
expect(generators.length).toBeGreaterThan(0);
178+
for (const { target, ok, errors } of generators)
179+
{
180+
expect(ok, `${target} shader generation failed: ${errors.join('; ')}`).toBe(true);
126181
}
127-
// Cleanup element and document
128-
elem.delete();
129-
doc.delete();
130182
});
131183
});

javascript/MaterialXTest/codeExamples.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { expect } from 'chai';
22
import Module from './_build/JsMaterialXCore.js';
3-
import { getMtlxStrings } from './testHelpers';
3+
import { getMtlxStrings } from './testHelpers.js';
44

55
describe('Code Examples', () =>
66
{

javascript/MaterialXTest/customBindings.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { expect } from 'chai';
22
import Module from './_build/JsMaterialXCore.js';
3-
import { getMtlxStrings } from './testHelpers';
3+
import { getMtlxStrings } from './testHelpers.js';
44

55
describe('Custom Bindings', () =>
66
{

0 commit comments

Comments
 (0)