WESL Logo

#wgsl-test

Test WGSL and WESL shaders with the CLI or vitest.

#Installation

npm install wgsl-test

#Native WESL Testing

Write tests directly in WESL with the @test attribute. Minimal boilerplate, assertions run on the GPU:

/// interp_test.wesl
import package::interp::smootherstep;
import wgsl_test::expectNear;

@test
fn smootherstepQuarter() {
  expectNear(smootherstep(0.0, 1.0, 0.25), 0.103516);
}

See the assertion functions reference for expect, expectNear, expectNearV2f, and more.

#Annotated Resources

Declare GPU resources inline with @buffer, @test_texture, and @sampler. wgsl-test assigns bindings and builds the bind group automatically:

import wgsl_test::expectNear;

@buffer var<storage, read_write> data: array<f32, 4>;

@test fn write_and_read() {
  data[0] = 42.0;
  expectNear(data[0], 42.0);
}

Fragment snapshots can combine textures and samplers the same way:

@test_texture(checkerboard, 256, 256, 16) var tex: texture_2d<f32>;
@sampler(nearest)                         var samp: sampler;

@fragment @extent(256, 256) @snapshot
fn test_checker(@builtin(position) pos: vec4f) -> @location(0) vec4f {
  let uv = pos.xy / 256.0;
  return textureSampleLevel(tex, samp, uv, 0.0);
}

Built-in texture sources: checkerboard, gradient, radial_gradient, color_bars, edge_pattern, noise, solid, lemur. Sampler filter options: linear, nearest.

#CLI

Run tests directly from the command line:

wgsl-test run                      # discover **/*.test.wesl, run all
wgsl-test run path/to/test.wesl    # run specific file(s)
wgsl-test run --projectDir ./foo   # specify project root

#Vitest Integration

Or run GPU shader tests from vitest with a minimal TypeScript wrapper:

import { getGPUDevice, testWesl } from "wgsl-test";

const device = await getGPUDevice();

await testWesl({ device, moduleName: "interp_test" });

#Visual Regression Testing

Test complete rendered images in vitest:

import { expectFragmentImage } from "wgsl-test";
import { imageMatcher } from "vitest-image-snapshot";

imageMatcher(); // setup once

test("blur shader matches snapshot", async () => {
  await expectFragmentImage(device, "effects/blur.wgsl");
  // snapshot compared against __image_snapshots__/effects-blur.png
});

Update snapshots with vitest -u.

See the visual regression testing reference for details.

#Testing Compute Shaders

If you want to validate results in TypeScript, use testCompute() for compute shader tests. Declare each output as a @buffer; the contents come back keyed by var name. Unwritten slots show as -999.0 (sentinel pre-fill).

import { testCompute, getGPUDevice } from "wgsl-test";

const device = await getGPUDevice();

const src = ` // test function can be an inline WESL string or a .wesl file
  import package::hash::lowbias32;

  @buffer var<storage, read_write> results: array<u32, 2>;

  @compute @workgroup_size(1)
  fn main() {
    results[0] = lowbias32(0u);
    results[1] = lowbias32(42u);
  }
`;

const { results } = await testCompute({ device, src });
// results = [0, 388445122]

Multiple @buffer declarations are returned together in the same record:

const r = await testCompute({ device, src: `
  @buffer var<storage, read_write> sums:     array<u32, 2>;
  @buffer var<storage, read_write> products: array<u32, 2>;
  @compute @workgroup_size(1) fn main() { /* ... */ }
`});
// r.sums, r.products