Fake Canvas + WebGL Layer: Canvas2D, WebGL1, WebGL2 contexts #8

Closed
opened 2026-06-17 13:37:43 +00:00 by Artur · 1 comment
Owner

Goal

Implement fake Canvas2D, WebGL1, and WebGL2 contexts. All methods exist, return plausible values, do nothing.

What to Build

src/fakes/canvas.ts

FakeCanvas2DContext

class FakeCanvas2DContext {
  // State (set but not used for rendering)
  fillStyle: string = '#000000';
  strokeStyle: string = '#000000';
  globalAlpha: number = 1.0;
  globalCompositeOperation: string = 'source-over';
  lineWidth: number = 1;
  lineCap: string = 'butt';
  lineJoin: string = 'miter';
  font: string = '10px sans-serif';
  textAlign: string = 'start';
  textBaseline: string = 'alphabetic';
  // ... additional state properties

  // NOOP methods (all exist, do nothing)
  fillRect(x, y, w, h): void {}
  strokeRect(x, y, w, h): void {}
  clearRect(x, y, w, h): void {}
  fillText(text, x, y, maxWidth?): void {}
  strokeText(text, x, y, maxWidth?): void {}
  beginPath(): void {}
  closePath(): void {}
  moveTo(x, y): void {}
  lineTo(x, y): void {}
  arc(x, y, r, startAngle, endAngle, ccw?): void {}
  // ... all other methods NOOP

  // Methods that return values:
  measureText(text: string): TextMetrics {
    return { width: text.length * 8, actualBoundingBoxAscent: 8, ... };
  }
  getImageData(sx, sy, sw, sh): ImageData {
    return new ImageData(sw, sh); // transparent pixels
  }
  createImageData(w, h): ImageData;
  createLinearGradient(x0, y0, x1, y1): CanvasGradient;
  createRadialGradient(x0, y0, r0, x1, y1, r1): CanvasGradient;
  createPattern(image, repetition): CanvasPattern | null;
  isPointInPath(x, y, fillRule?): boolean { return false; }
  isPointInStroke(x, y): boolean { return false; }
  toDataURL(type?, quality?): string {
    return 'data:image/png;base64,iVBOR...'; // minimal valid PNG
  }
  toBlob(callback, type?, quality?): void {
    callback(new Blob([]));
  }
  save(): void {}
  restore(): void {}
  scale(x, y): void {}
  rotate(angle): void {}
  translate(x, y): void {}
  transform(a, b, c, d, e, f): void {}
  setTransform(a, b, c, d, e, f): void {}
  resetTransform(): void {}
  // canvas, drawImage, putImageData also NOOP
}

FakeWebGLRenderingContext

class FakeWebGLRenderingContext {
  // Constants (from WebGL spec)
  readonly VENDOR = 0x1F00;
  readonly RENDERER = 0x1F01;
  readonly COMPILE_STATUS = 0x8B81;
  readonly LINK_STATUS = 0x8B82;
  // ... all GL constants as readonly properties

  getParameter(pname: number): any {
    switch(pname) {
      case this.VENDOR: return 'WebKit';
      case this.RENDERER: return 'WebKit WebGL';
      case this.VERSION: return 'WebGL 1.0';
      case this.SHADING_LANGUAGE_VERSION: return 'WebGL GLSL ES 1.0';
      case this.MAX_TEXTURE_SIZE: return 4096;
      case this.MAX_CUBE_MAP_TEXTURE_SIZE: return 4096;
      case this.MAX_RENDERBUFFER_SIZE: return 4096;
      case this.MAX_TEXTURE_IMAGE_UNITS: return 8;
      case this.MAX_VERTEX_TEXTURE_IMAGE_UNITS: return 4;
      case this.MAX_COMBINED_TEXTURE_IMAGE_UNITS: return 8;
      case this.MAX_VERTEX_ATTRIBS: return 16;
      case this.MAX_VARYING_VECTORS: return 10;
      case this.MAX_FRAGMENT_UNIFORM_VECTORS: return 16;
      case this.MAX_VERTEX_UNIFORM_VECTORS: return 16;
      case this.ALIASED_POINT_SIZE_RANGE: return [1, 1024];
      case this.ALIASED_LINE_WIDTH_RANGE: return [1, 1];
      default: return 0;
    }
  }

  getExtension(name: string): object | null {
    // Return fake extensions for common ones:
    // - 'WEBGL_lose_context': { loseContext: () => {}, restoreContext: () => {} }
    // - 'EXT_texture_filter_anisotropic': { TEXTURE_MAX_ANISOTROPY_EXT: 0x84FE, MAX_TEXTURE_MAX_ANISOTROPY_EXT: 0x84FF }
    // - 'OES_texture_float', 'OES_texture_half_float': true-ish objects
    // - Others: null
  }

  getSupportedExtensions(): string[] {
    return ['WEBGL_lose_context', 'EXT_texture_filter_anisotropic',
            'OES_texture_float', 'OES_texture_half_float', 'WEBGL_debug_renderer_info'];
  }

  // All NOOP methods:
  createShader(type): WebGLShader { return {}; }
  shaderSource(shader, source): void {}
  compileShader(shader): void {}
  getShaderParameter(shader, pname): boolean { return true; }
  getShaderInfoLog(shader): string { return ''; }
  createProgram(): WebGLProgram { return {}; }
  attachShader(program, shader): void {}
  linkProgram(program): void {}
  getProgramParameter(program, pname): boolean { return true; }
  getProgramInfoLog(program): string { return ''; }
  useProgram(program): void {}
  createBuffer(): WebGLBuffer { return {}; }
  bindBuffer(target, buffer): void {}
  bufferData(target, data, usage): void {}
  createTexture(): WebGLTexture { return {}; }
  bindTexture(target, texture): void {}
  texImage2D(...): void {}
  texParameteri(target, pname, param): void {}
  createFramebuffer(): WebGLFramebuffer { return {}; }
  bindFramebuffer(target, framebuffer): void {}
  framebufferTexture2D(...): void {}
  drawArrays(mode, first, count): void {}
  drawElements(mode, count, type, offset): void {}
  readPixels(x, y, width, height, format, type, pixels): void { /* fill zeros */ }
  clear(mask): void {}
  clearColor(r, g, b, a): void {}
  viewport(x, y, w, h): void {}
  enable(cap): void {}
  disable(cap): void {}
  // ... all remaining WebGL methods NOOP
}

class FakeWebGL2RenderingContext extends FakeWebGLRenderingContext {
  // Additional WebGL2 methods NOOP
  // Different getParameter values for WebGL2 specific params
}

Installation

// In createIsolatedContext():
HTMLCanvasElement.prototype.getContext = function(contextId: string, ...args: any[]) {
  switch(contextId) {
    case '2d': return new FakeCanvas2DContext();
    case 'webgl': case 'experimental-webgl': return new FakeWebGLRenderingContext();
    case 'webgl2': case 'experimental-webgl2': return new FakeWebGL2RenderingContext();
    default: return null;
  }
};

Tests

Unit Tests

Test Verifies
canvas2d.basic.test.ts getContext('2d') returns context
canvas2d.state.test.ts fillStyle, strokeStyle, font readable/writable
canvas2d.measure-text.test.ts measureText returns width based on text length
canvas2d.get-image-data.test.ts getImageData returns ImageData with correct size
canvas2d.to-data-url.test.ts toDataURL returns valid data URL string
canvas2d.to-blob.test.ts toBlob fires callback with blob
canvas2d.all-noop.test.ts All drawing methods can be called without error
webgl.basic.test.ts getContext('webgl') returns context
webgl.parameter.test.ts getParameter(VENDOR) returns 'WebKit'
webgl.shader.test.ts createShader + shaderSource + compileShader works
webgl.program.test.ts createProgram + attachShader + linkProgram works
webgl.draw.test.ts drawArrays/drawElements are NOOP
webgl.extension.test.ts getExtension('WEBGL_lose_context') returns object
webgl2.basic.test.ts getContext('webgl2') returns context
webgl.unsupported.test.ts getContext('bitmaprenderer') returns null

Edge Cases

Test Verifies
canvas2d.save-restore.test.ts save/restore stack works
canvas2d.transform.test.ts All transform methods callable
webgl.read-pixels.test.ts readPixels fills zeros, no crash
webgl.get-extension-null.test.ts Unknown extension returns null

Definition of Done

  • src/fakes/canvas.ts implemented
  • FakeCanvas2DContext with all methods (NOOP + value returns)
  • FakeWebGLRenderingContext with all methods
  • FakeWebGL2RenderingContext extending WebGL1
  • getContext() patched on HTMLCanvasElement.prototype
  • All tests pass
  • 100% line + branch coverage
## Goal Implement fake Canvas2D, WebGL1, and WebGL2 contexts. All methods exist, return plausible values, do nothing. ## What to Build ### src/fakes/canvas.ts #### FakeCanvas2DContext ``` class FakeCanvas2DContext { // State (set but not used for rendering) fillStyle: string = '#000000'; strokeStyle: string = '#000000'; globalAlpha: number = 1.0; globalCompositeOperation: string = 'source-over'; lineWidth: number = 1; lineCap: string = 'butt'; lineJoin: string = 'miter'; font: string = '10px sans-serif'; textAlign: string = 'start'; textBaseline: string = 'alphabetic'; // ... additional state properties // NOOP methods (all exist, do nothing) fillRect(x, y, w, h): void {} strokeRect(x, y, w, h): void {} clearRect(x, y, w, h): void {} fillText(text, x, y, maxWidth?): void {} strokeText(text, x, y, maxWidth?): void {} beginPath(): void {} closePath(): void {} moveTo(x, y): void {} lineTo(x, y): void {} arc(x, y, r, startAngle, endAngle, ccw?): void {} // ... all other methods NOOP // Methods that return values: measureText(text: string): TextMetrics { return { width: text.length * 8, actualBoundingBoxAscent: 8, ... }; } getImageData(sx, sy, sw, sh): ImageData { return new ImageData(sw, sh); // transparent pixels } createImageData(w, h): ImageData; createLinearGradient(x0, y0, x1, y1): CanvasGradient; createRadialGradient(x0, y0, r0, x1, y1, r1): CanvasGradient; createPattern(image, repetition): CanvasPattern | null; isPointInPath(x, y, fillRule?): boolean { return false; } isPointInStroke(x, y): boolean { return false; } toDataURL(type?, quality?): string { return 'data:image/png;base64,iVBOR...'; // minimal valid PNG } toBlob(callback, type?, quality?): void { callback(new Blob([])); } save(): void {} restore(): void {} scale(x, y): void {} rotate(angle): void {} translate(x, y): void {} transform(a, b, c, d, e, f): void {} setTransform(a, b, c, d, e, f): void {} resetTransform(): void {} // canvas, drawImage, putImageData also NOOP } ``` #### FakeWebGLRenderingContext ``` class FakeWebGLRenderingContext { // Constants (from WebGL spec) readonly VENDOR = 0x1F00; readonly RENDERER = 0x1F01; readonly COMPILE_STATUS = 0x8B81; readonly LINK_STATUS = 0x8B82; // ... all GL constants as readonly properties getParameter(pname: number): any { switch(pname) { case this.VENDOR: return 'WebKit'; case this.RENDERER: return 'WebKit WebGL'; case this.VERSION: return 'WebGL 1.0'; case this.SHADING_LANGUAGE_VERSION: return 'WebGL GLSL ES 1.0'; case this.MAX_TEXTURE_SIZE: return 4096; case this.MAX_CUBE_MAP_TEXTURE_SIZE: return 4096; case this.MAX_RENDERBUFFER_SIZE: return 4096; case this.MAX_TEXTURE_IMAGE_UNITS: return 8; case this.MAX_VERTEX_TEXTURE_IMAGE_UNITS: return 4; case this.MAX_COMBINED_TEXTURE_IMAGE_UNITS: return 8; case this.MAX_VERTEX_ATTRIBS: return 16; case this.MAX_VARYING_VECTORS: return 10; case this.MAX_FRAGMENT_UNIFORM_VECTORS: return 16; case this.MAX_VERTEX_UNIFORM_VECTORS: return 16; case this.ALIASED_POINT_SIZE_RANGE: return [1, 1024]; case this.ALIASED_LINE_WIDTH_RANGE: return [1, 1]; default: return 0; } } getExtension(name: string): object | null { // Return fake extensions for common ones: // - 'WEBGL_lose_context': { loseContext: () => {}, restoreContext: () => {} } // - 'EXT_texture_filter_anisotropic': { TEXTURE_MAX_ANISOTROPY_EXT: 0x84FE, MAX_TEXTURE_MAX_ANISOTROPY_EXT: 0x84FF } // - 'OES_texture_float', 'OES_texture_half_float': true-ish objects // - Others: null } getSupportedExtensions(): string[] { return ['WEBGL_lose_context', 'EXT_texture_filter_anisotropic', 'OES_texture_float', 'OES_texture_half_float', 'WEBGL_debug_renderer_info']; } // All NOOP methods: createShader(type): WebGLShader { return {}; } shaderSource(shader, source): void {} compileShader(shader): void {} getShaderParameter(shader, pname): boolean { return true; } getShaderInfoLog(shader): string { return ''; } createProgram(): WebGLProgram { return {}; } attachShader(program, shader): void {} linkProgram(program): void {} getProgramParameter(program, pname): boolean { return true; } getProgramInfoLog(program): string { return ''; } useProgram(program): void {} createBuffer(): WebGLBuffer { return {}; } bindBuffer(target, buffer): void {} bufferData(target, data, usage): void {} createTexture(): WebGLTexture { return {}; } bindTexture(target, texture): void {} texImage2D(...): void {} texParameteri(target, pname, param): void {} createFramebuffer(): WebGLFramebuffer { return {}; } bindFramebuffer(target, framebuffer): void {} framebufferTexture2D(...): void {} drawArrays(mode, first, count): void {} drawElements(mode, count, type, offset): void {} readPixels(x, y, width, height, format, type, pixels): void { /* fill zeros */ } clear(mask): void {} clearColor(r, g, b, a): void {} viewport(x, y, w, h): void {} enable(cap): void {} disable(cap): void {} // ... all remaining WebGL methods NOOP } class FakeWebGL2RenderingContext extends FakeWebGLRenderingContext { // Additional WebGL2 methods NOOP // Different getParameter values for WebGL2 specific params } ``` ### Installation ```typescript // In createIsolatedContext(): HTMLCanvasElement.prototype.getContext = function(contextId: string, ...args: any[]) { switch(contextId) { case '2d': return new FakeCanvas2DContext(); case 'webgl': case 'experimental-webgl': return new FakeWebGLRenderingContext(); case 'webgl2': case 'experimental-webgl2': return new FakeWebGL2RenderingContext(); default: return null; } }; ``` ## Tests ### Unit Tests | Test | Verifies | |------|----------| | canvas2d.basic.test.ts | getContext('2d') returns context | | canvas2d.state.test.ts | fillStyle, strokeStyle, font readable/writable | | canvas2d.measure-text.test.ts | measureText returns width based on text length | | canvas2d.get-image-data.test.ts | getImageData returns ImageData with correct size | | canvas2d.to-data-url.test.ts | toDataURL returns valid data URL string | | canvas2d.to-blob.test.ts | toBlob fires callback with blob | | canvas2d.all-noop.test.ts | All drawing methods can be called without error | | webgl.basic.test.ts | getContext('webgl') returns context | | webgl.parameter.test.ts | getParameter(VENDOR) returns 'WebKit' | | webgl.shader.test.ts | createShader + shaderSource + compileShader works | | webgl.program.test.ts | createProgram + attachShader + linkProgram works | | webgl.draw.test.ts | drawArrays/drawElements are NOOP | | webgl.extension.test.ts | getExtension('WEBGL_lose_context') returns object | | webgl2.basic.test.ts | getContext('webgl2') returns context | | webgl.unsupported.test.ts | getContext('bitmaprenderer') returns null | ### Edge Cases | Test | Verifies | |------|----------| | canvas2d.save-restore.test.ts | save/restore stack works | | canvas2d.transform.test.ts | All transform methods callable | | webgl.read-pixels.test.ts | readPixels fills zeros, no crash | | webgl.get-extension-null.test.ts | Unknown extension returns null | ## Definition of Done - [ ] src/fakes/canvas.ts implemented - [ ] FakeCanvas2DContext with all methods (NOOP + value returns) - [ ] FakeWebGLRenderingContext with all methods - [ ] FakeWebGL2RenderingContext extending WebGL1 - [ ] getContext() patched on HTMLCanvasElement.prototype - [ ] All tests pass - [ ] 100% line + branch coverage
Author
Owner

Fake Canvas + WebGL Layer: Canvas2D, WebGL1, WebGL2 contexts. Implementiert in src/fakes/. Tests: canvas.test.ts (120+ Tests).

Fake Canvas + WebGL Layer: Canvas2D, WebGL1, WebGL2 contexts. ✅ Implementiert in src/fakes/. Tests: canvas.test.ts (120+ Tests).
Artur closed this issue 2026-06-18 06:28:03 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
glow-all/true-headless-browser#8
No description provided.