Add simple tests & remove !
This commit is contained in:
parent
0735dd764f
commit
be0a49a11f
17 changed files with 1418 additions and 124 deletions
5
.claude/settings.json
Normal file
5
.claude/settings.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"enabledPlugins": {
|
||||||
|
"frontend-design@claude-plugins-official": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -23,7 +23,8 @@
|
||||||
<meta property="og:image:height" content="1920" />
|
<meta property="og:image:height" content="1920" />
|
||||||
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<link rel="apple-touch-icon" href="/favicon.svg" />
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||||
|
<link rel="apple-touch-icon" href="/apple-touch-icon-180x180.png" />
|
||||||
<link rel="manifest" href="/manifest.webmanifest" />
|
<link rel="manifest" href="/manifest.webmanifest" />
|
||||||
|
|
||||||
<title>Just a bunch of blobs</title>
|
<title>Just a bunch of blobs</title>
|
||||||
|
|
|
||||||
1088
package-lock.json
generated
1088
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -10,6 +10,9 @@
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"lint": "eslint --fix \"src/**/*.ts\" && prettier --write \"src/**/*.{ts,scss,json,html}\"",
|
"lint": "eslint --fix \"src/**/*.ts\" && prettier --write \"src/**/*.{ts,scss,json,html}\"",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest",
|
||||||
|
"generate-icons": "pwa-assets-generator",
|
||||||
"update": "ncu"
|
"update": "ncu"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -34,6 +37,7 @@
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@ianvs/prettier-plugin-sort-imports": "^4.7.1",
|
"@ianvs/prettier-plugin-sort-imports": "^4.7.1",
|
||||||
"@types/node": "^25.6.0",
|
"@types/node": "^25.6.0",
|
||||||
|
"@vite-pwa/assets-generator": "^1.0.2",
|
||||||
"@webgpu/types": "^0.1.69",
|
"@webgpu/types": "^0.1.69",
|
||||||
"eslint": "^10.3.0",
|
"eslint": "^10.3.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
|
|
@ -46,6 +50,7 @@
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
"typescript-eslint": "^8.59.1",
|
"typescript-eslint": "^8.59.1",
|
||||||
"vite": "^8.0.10",
|
"vite": "^8.0.10",
|
||||||
"vite-plugin-singlefile": "^2.3.3"
|
"vite-plugin-singlefile": "^2.3.3",
|
||||||
|
"vitest": "^4.1.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,28 @@
|
||||||
"src": "/favicon.svg",
|
"src": "/favicon.svg",
|
||||||
"sizes": "any",
|
"sizes": "any",
|
||||||
"type": "image/svg+xml",
|
"type": "image/svg+xml",
|
||||||
"purpose": "any maskable"
|
"purpose": "any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/pwa-64x64.png",
|
||||||
|
"sizes": "64x64",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/pwa-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/pwa-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/maskable-icon-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"categories": ["entertainment", "graphics"],
|
"categories": ["entertainment", "graphics"],
|
||||||
|
|
|
||||||
9
pwa-assets.config.ts
Normal file
9
pwa-assets.config.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import {
|
||||||
|
defineConfig,
|
||||||
|
minimal2023Preset as preset,
|
||||||
|
} from '@vite-pwa/assets-generator/config';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
preset,
|
||||||
|
images: ['public/favicon.svg'],
|
||||||
|
});
|
||||||
|
|
@ -28,10 +28,7 @@ export default class GameLoop {
|
||||||
private readonly diffusionPipeline: DiffusionPipeline;
|
private readonly diffusionPipeline: DiffusionPipeline;
|
||||||
|
|
||||||
private hasFinished = false;
|
private hasFinished = false;
|
||||||
private readonly hasFinishedPromise: Promise<void> = new Promise(
|
private readonly finished = Promise.withResolvers<void>();
|
||||||
(resolve) => (this.resolveHasFinished = resolve)
|
|
||||||
);
|
|
||||||
private resolveHasFinished!: () => void;
|
|
||||||
|
|
||||||
private activePointerId: number | null = null;
|
private activePointerId: number | null = null;
|
||||||
|
|
||||||
|
|
@ -120,7 +117,7 @@ export default class GameLoop {
|
||||||
public async start(): Promise<void> {
|
public async start(): Promise<void> {
|
||||||
requestAnimationFrame(this.render.bind(this));
|
requestAnimationFrame(this.render.bind(this));
|
||||||
requestAnimationFrame(this.updateCounts.bind(this));
|
requestAnimationFrame(this.updateCounts.bind(this));
|
||||||
return this.hasFinishedPromise;
|
return this.finished.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateCounts(): Promise<void> {
|
private async updateCounts(): Promise<void> {
|
||||||
|
|
@ -152,7 +149,7 @@ export default class GameLoop {
|
||||||
|
|
||||||
private async render(time: DOMHighResTimeStamp) {
|
private async render(time: DOMHighResTimeStamp) {
|
||||||
if (this.hasFinished) {
|
if (this.hasFinished) {
|
||||||
this.resolveHasFinished();
|
this.finished.resolve();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -245,7 +242,7 @@ export default class GameLoop {
|
||||||
|
|
||||||
public async destroy() {
|
public async destroy() {
|
||||||
this.hasFinished = true;
|
this.hasFinished = true;
|
||||||
await this.hasFinishedPromise;
|
await this.finished.promise;
|
||||||
|
|
||||||
this.copyPipeline?.destroy();
|
this.copyPipeline?.destroy();
|
||||||
this.agentGenerationPipeline?.destroy();
|
this.agentGenerationPipeline?.destroy();
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { vec2 } from 'gl-matrix';
|
||||||
|
|
||||||
import { clamp } from '../../utils/clamp';
|
import { clamp } from '../../utils/clamp';
|
||||||
import { smartCompile } from '../../utils/graphics/smart-compile';
|
import { smartCompile } from '../../utils/graphics/smart-compile';
|
||||||
import { last } from '../../utils/last';
|
|
||||||
import { CommonState } from '../common-state/common-state';
|
import { CommonState } from '../common-state/common-state';
|
||||||
import { BrushSettings } from './brush-settings';
|
import { BrushSettings } from './brush-settings';
|
||||||
import shader from './brush.wgsl?raw';
|
import shader from './brush.wgsl?raw';
|
||||||
|
|
@ -188,7 +187,7 @@ export class BrushPipeline {
|
||||||
result.push(position);
|
result.push(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
result.push(last(points)!);
|
result.push(points[points.length - 1]);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
22
src/utils/format-number.test.ts
Normal file
22
src/utils/format-number.test.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { formatNumber } from './format-number';
|
||||||
|
|
||||||
|
describe('formatNumber', () => {
|
||||||
|
it('renders integers without decimals', () => {
|
||||||
|
expect(formatNumber(42)).toBe('42 ');
|
||||||
|
});
|
||||||
|
it('renders fractional values with two decimals', () => {
|
||||||
|
expect(formatNumber(3.14159)).toBe('3.14 ');
|
||||||
|
});
|
||||||
|
it('renders thousands compactly', () => {
|
||||||
|
expect(formatNumber(2500)).toBe('2.5 thousand ');
|
||||||
|
});
|
||||||
|
it('renders millions compactly', () => {
|
||||||
|
expect(formatNumber(1_500_000)).toBe('1.5 million ');
|
||||||
|
});
|
||||||
|
it('appends the unit when provided', () => {
|
||||||
|
expect(formatNumber(5, 'agents')).toBe('5 agents');
|
||||||
|
expect(formatNumber(2_000_000, 'agents')).toBe('2.0 million agents');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -13,7 +13,11 @@ export const generateNoise = ({
|
||||||
height: number;
|
height: number;
|
||||||
}): GPUTextureView => {
|
}): GPUTextureView => {
|
||||||
const cacheKey = `${width}x${height}`;
|
const cacheKey = `${width}x${height}`;
|
||||||
if (!textureCache.has(cacheKey)) {
|
const cached = textureCache.get(cacheKey);
|
||||||
|
if (cached) {
|
||||||
|
return cached.createView();
|
||||||
|
}
|
||||||
|
|
||||||
const { buffer, vertex } = setUpFullScreenQuad(device);
|
const { buffer, vertex } = setUpFullScreenQuad(device);
|
||||||
const vertexBuffer = buffer;
|
const vertexBuffer = buffer;
|
||||||
|
|
||||||
|
|
@ -81,7 +85,5 @@ export const generateNoise = ({
|
||||||
|
|
||||||
device.queue.submit([commandEncoder.finish()]);
|
device.queue.submit([commandEncoder.finish()]);
|
||||||
textureCache.set(cacheKey, colorTexture);
|
textureCache.set(cacheKey, colorTexture);
|
||||||
}
|
return colorTexture.createView();
|
||||||
|
|
||||||
return textureCache.get(cacheKey)!.createView();
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,39 +3,29 @@ import { vec2 } from 'gl-matrix';
|
||||||
import { CopyPipeline } from '../../pipelines/copy/copy-pipeline';
|
import { CopyPipeline } from '../../pipelines/copy/copy-pipeline';
|
||||||
|
|
||||||
export class ResizableTexture {
|
export class ResizableTexture {
|
||||||
private texture!: GPUTexture;
|
private texture: GPUTexture;
|
||||||
private textureView!: GPUTextureView;
|
private textureView: GPUTextureView;
|
||||||
|
private size: vec2;
|
||||||
private readonly copyPipeline: CopyPipeline;
|
private readonly copyPipeline: CopyPipeline;
|
||||||
private size: vec2 | null = null;
|
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly device: GPUDevice,
|
private readonly device: GPUDevice,
|
||||||
size: vec2
|
size: vec2
|
||||||
) {
|
) {
|
||||||
this.copyPipeline = new CopyPipeline(this.device);
|
this.copyPipeline = new CopyPipeline(this.device);
|
||||||
this.resize(size);
|
this.size = size;
|
||||||
|
this.texture = this.createTexture(size);
|
||||||
|
this.textureView = this.texture.createView();
|
||||||
}
|
}
|
||||||
|
|
||||||
public resize(size: vec2): void {
|
public resize(size: vec2): void {
|
||||||
if (this.size !== null && vec2.equals(this.size, size)) {
|
if (vec2.equals(this.size, size)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newTexture = this.device.createTexture({
|
const newTexture = this.createTexture(size);
|
||||||
format: 'rgba16float',
|
|
||||||
size: {
|
|
||||||
width: size[0],
|
|
||||||
height: size[1],
|
|
||||||
},
|
|
||||||
usage:
|
|
||||||
GPUTextureUsage.STORAGE_BINDING |
|
|
||||||
GPUTextureUsage.TEXTURE_BINDING |
|
|
||||||
GPUTextureUsage.RENDER_ATTACHMENT,
|
|
||||||
});
|
|
||||||
|
|
||||||
const newTextureView = newTexture.createView();
|
const newTextureView = newTexture.createView();
|
||||||
|
|
||||||
if (this.size) {
|
|
||||||
const commandEncoder = this.device.createCommandEncoder();
|
const commandEncoder = this.device.createCommandEncoder();
|
||||||
this.copyPipeline.execute(
|
this.copyPipeline.execute(
|
||||||
commandEncoder,
|
commandEncoder,
|
||||||
|
|
@ -45,7 +35,6 @@ export class ResizableTexture {
|
||||||
);
|
);
|
||||||
this.device.queue.submit([commandEncoder.finish()]);
|
this.device.queue.submit([commandEncoder.finish()]);
|
||||||
this.texture.destroy();
|
this.texture.destroy();
|
||||||
}
|
|
||||||
|
|
||||||
this.size = size;
|
this.size = size;
|
||||||
this.texture = newTexture;
|
this.texture = newTexture;
|
||||||
|
|
@ -60,4 +49,15 @@ export class ResizableTexture {
|
||||||
this.texture.destroy();
|
this.texture.destroy();
|
||||||
this.copyPipeline.destroy();
|
this.copyPipeline.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private createTexture(size: vec2): GPUTexture {
|
||||||
|
return this.device.createTexture({
|
||||||
|
format: 'rgba16float',
|
||||||
|
size: { width: size[0], height: size[1] },
|
||||||
|
usage:
|
||||||
|
GPUTextureUsage.STORAGE_BINDING |
|
||||||
|
GPUTextureUsage.TEXTURE_BINDING |
|
||||||
|
GPUTextureUsage.RENDER_ATTACHMENT,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
42
src/utils/hsl.test.ts
Normal file
42
src/utils/hsl.test.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { hsl } from './hsl';
|
||||||
|
|
||||||
|
describe('hsl', () => {
|
||||||
|
it('produces pure red at hue 0', () => {
|
||||||
|
const [r, g, b] = hsl(0, 100, 50);
|
||||||
|
expect(r).toBeCloseTo(1);
|
||||||
|
expect(g).toBeCloseTo(0);
|
||||||
|
expect(b).toBeCloseTo(0);
|
||||||
|
});
|
||||||
|
it('produces pure green at hue 120', () => {
|
||||||
|
const [r, g, b] = hsl(120, 100, 50);
|
||||||
|
expect(r).toBeCloseTo(0);
|
||||||
|
expect(g).toBeCloseTo(1);
|
||||||
|
expect(b).toBeCloseTo(0);
|
||||||
|
});
|
||||||
|
it('produces pure blue at hue 240', () => {
|
||||||
|
const [r, g, b] = hsl(240, 100, 50);
|
||||||
|
expect(r).toBeCloseTo(0);
|
||||||
|
expect(g).toBeCloseTo(0);
|
||||||
|
expect(b).toBeCloseTo(1);
|
||||||
|
});
|
||||||
|
it('produces gray at saturation 0', () => {
|
||||||
|
const [r, g, b] = hsl(180, 0, 50);
|
||||||
|
expect(r).toBeCloseTo(0.5);
|
||||||
|
expect(g).toBeCloseTo(0.5);
|
||||||
|
expect(b).toBeCloseTo(0.5);
|
||||||
|
});
|
||||||
|
it('produces black at lightness 0', () => {
|
||||||
|
const [r, g, b] = hsl(0, 100, 0);
|
||||||
|
expect(r).toBe(0);
|
||||||
|
expect(g).toBe(0);
|
||||||
|
expect(b).toBe(0);
|
||||||
|
});
|
||||||
|
it('produces white at lightness 100', () => {
|
||||||
|
const [r, g, b] = hsl(0, 100, 100);
|
||||||
|
expect(r).toBeCloseTo(1);
|
||||||
|
expect(g).toBeCloseTo(1);
|
||||||
|
expect(b).toBeCloseTo(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
export function last<T>(a: Array<T>): T | null {
|
|
||||||
return a.length > 0 ? a[a.length - 1] : null;
|
|
||||||
}
|
|
||||||
63
src/utils/math.test.ts
Normal file
63
src/utils/math.test.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { clamp, clamp01 } from './clamp';
|
||||||
|
import { exponentialDecay } from './exponential-decay';
|
||||||
|
import { mix } from './mix';
|
||||||
|
|
||||||
|
describe('clamp', () => {
|
||||||
|
it('returns value when within bounds', () => {
|
||||||
|
expect(clamp(5, 0, 10)).toBe(5);
|
||||||
|
});
|
||||||
|
it('clamps below to lower bound', () => {
|
||||||
|
expect(clamp(-3, 0, 10)).toBe(0);
|
||||||
|
});
|
||||||
|
it('clamps above to upper bound', () => {
|
||||||
|
expect(clamp(42, 0, 10)).toBe(10);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('clamp01', () => {
|
||||||
|
it('passes through values in [0, 1]', () => {
|
||||||
|
expect(clamp01(0.25)).toBe(0.25);
|
||||||
|
});
|
||||||
|
it('clamps negatives to 0', () => {
|
||||||
|
expect(clamp01(-1)).toBe(0);
|
||||||
|
});
|
||||||
|
it('clamps above 1 to 1', () => {
|
||||||
|
expect(clamp01(2)).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('mix', () => {
|
||||||
|
it('returns from at q=0', () => {
|
||||||
|
expect(mix(10, 20, 0)).toBe(10);
|
||||||
|
});
|
||||||
|
it('returns to at q=1', () => {
|
||||||
|
expect(mix(10, 20, 1)).toBe(20);
|
||||||
|
});
|
||||||
|
it('interpolates at q=0.5', () => {
|
||||||
|
expect(mix(10, 20, 0.5)).toBe(15);
|
||||||
|
});
|
||||||
|
it('extrapolates outside [0, 1]', () => {
|
||||||
|
expect(mix(0, 10, 2)).toBe(20);
|
||||||
|
expect(mix(0, 10, -1)).toBe(-10);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('exponentialDecay', () => {
|
||||||
|
it('returns nextValue when bias is 1', () => {
|
||||||
|
expect(exponentialDecay({ accumulator: 0, nextValue: 10, biasOfNextValue: 1 })).toBe(
|
||||||
|
10
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('returns accumulator when bias is 0', () => {
|
||||||
|
expect(exponentialDecay({ accumulator: 5, nextValue: 10, biasOfNextValue: 0 })).toBe(
|
||||||
|
5
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('blends with given bias', () => {
|
||||||
|
expect(
|
||||||
|
exponentialDecay({ accumulator: 0, nextValue: 10, biasOfNextValue: 0.25 })
|
||||||
|
).toBe(2.5);
|
||||||
|
});
|
||||||
|
});
|
||||||
41
src/utils/random.test.ts
Normal file
41
src/utils/random.test.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { beforeEach, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { Random } from './random';
|
||||||
|
|
||||||
|
describe('Random', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
Random.seed = 42;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('produces values in [0, 1)', () => {
|
||||||
|
for (let i = 0; i < 1000; i++) {
|
||||||
|
const v = Random.getRandom();
|
||||||
|
expect(v).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(v).toBeLessThan(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is deterministic for the same seed', () => {
|
||||||
|
Random.seed = 42;
|
||||||
|
const a = Array.from({ length: 8 }, () => Random.getRandom());
|
||||||
|
Random.seed = 42;
|
||||||
|
const b = Array.from({ length: 8 }, () => Random.getRandom());
|
||||||
|
expect(a).toEqual(b);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('produces different sequences for different seeds', () => {
|
||||||
|
Random.seed = 1;
|
||||||
|
const a = Array.from({ length: 4 }, () => Random.getRandom());
|
||||||
|
Random.seed = 2;
|
||||||
|
const b = Array.from({ length: 4 }, () => Random.getRandom());
|
||||||
|
expect(a).not.toEqual(b);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('randomBetween stays within [from, to)', () => {
|
||||||
|
for (let i = 0; i < 1000; i++) {
|
||||||
|
const v = Random.randomBetween(-10, 10);
|
||||||
|
expect(v).toBeGreaterThanOrEqual(-10);
|
||||||
|
expect(v).toBeLessThan(10);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
"lib": ["ES2024", "DOM", "DOM.Iterable"],
|
||||||
"types": ["@webgpu/types", "vite/client"],
|
"types": ["@webgpu/types", "vite/client"],
|
||||||
|
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vitest/config';
|
||||||
import { viteSingleFile } from 'vite-plugin-singlefile';
|
import { viteSingleFile } from 'vite-plugin-singlefile';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
|
@ -11,4 +11,8 @@ export default defineConfig({
|
||||||
server: {
|
server: {
|
||||||
open: true,
|
open: true,
|
||||||
},
|
},
|
||||||
|
test: {
|
||||||
|
environment: 'node',
|
||||||
|
include: ['src/**/*.test.ts'],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue