├── .prettierignore ├── .prettierrc ├── .gitignore ├── src ├── classes │ ├── Image.js │ ├── ImageBitmap.js │ ├── CanvasPattern.js │ ├── TextMetrics.js │ ├── CanvasGradient.js │ ├── Path2D.js │ ├── ImageData.js │ ├── DOMMatrix.js │ └── WebGLRenderingContext.js ├── index.js ├── mock │ ├── createCanvasEvent.js │ ├── createImageBitmap.js │ └── prototype.js └── window.js ├── .babelrc ├── __mocks__ └── worker.js ├── __tests__ ├── index.js ├── classes │ ├── CanvasRenderingContext2D.save.js │ ├── CanvasRenderingContext2D.restore.js │ ├── CanvasRenderingContext2D.beginPath.js │ ├── CanvasRenderingContext2D.closePath.js │ ├── CanvasRenderingContext2D.clearHitRegions.js │ ├── __snapshots__ │ │ ├── CanvasRenderingContext2D.__clearEvents.js.snap │ │ ├── CanvasRenderingContext2D.__clearDrawCalls.js.snap │ │ ├── CanvasRenderingContext2D.__clearPath.js.snap │ │ ├── CanvasRenderingContext2D.__getClippingPath.js.snap │ │ ├── CanvasRenderingContext2D.__getDrawCalls.js.snap │ │ └── CanvasRenderingContext2D.__getPath.js.snap │ ├── CanvasRenderingContext2D.__clearPath.js │ ├── CanvasRenderingContext2D.__clearEvents.js │ ├── CanvasRenderingContext2D.__clearDrawCalls.js │ ├── CanvasRenderingContext2D.removeHitRegion.js │ ├── CanvasRenderingContext2D.scrollPathIntoView.js │ ├── CanvasRenderingContext2D.lineTo.js │ ├── CanvasRenderingContext2D.moveTo.js │ ├── CanvasRenderingContext2D.fillText.js │ ├── CanvasRenderingContext2D.strokeText.js │ ├── CanvasRenderingContext2D.font.js │ ├── CanvasRenderingContext2D.rect.js │ ├── CanvasRenderingContext2D.js │ ├── CanvasRenderingContext2D.resetTransform.js │ ├── CanvasRenderingContext2D.fillRect.js │ ├── CanvasRenderingContext2D.clearRect.js │ ├── CanvasRenderingContext2D.strokeRect.js │ ├── CanvasRenderingContext2D.imageSmoothingEnabled.js │ ├── CanvasRenderingContext2D.quadraticCurveTo.js │ ├── CanvasRenderingContext2D.stroke.js │ ├── CanvasRenderingContext2D.getImageData.js │ ├── CanvasRenderingContext2D.lineCap.js │ ├── CanvasRenderingContext2D.isPointInStroke.js │ ├── CanvasRenderingContext2D.filter.js │ ├── CanvasRenderingContext2D.bezierCurveTo.js │ ├── CanvasRenderingContext2D.globalAlpha.js │ ├── CanvasRenderingContext2D.imageSmoothingQuality.js │ ├── CanvasRenderingContext2D.textAlign.js │ ├── TextMetrics.js │ ├── CanvasRenderingContext2D.shadowOffsetX.js │ ├── CanvasRenderingContext2D.shadowOffsetY.js │ ├── CanvasRenderingContext2D.clip.js │ ├── CanvasRenderingContext2D.fill.js │ ├── CanvasRenderingContext2D.rotate.js │ ├── CanvasRenderingContext2D.lineJoin.js │ ├── CanvasRenderingContext2D.direction.js │ ├── CanvasRenderingContext2D.lineDashOffset.js │ ├── CanvasRenderingContext2D.addHitRegion.js │ ├── CanvasRenderingContext2D.textBaseline.js │ ├── CanvasRenderingContext2D.scale.js │ ├── CanvasRenderingContext2D.translate.js │ ├── CanvasRenderingContext2D.measureText.js │ ├── CanvasRenderingContext2D.shadowColor.js │ ├── CanvasRenderingContext2D.shadowBlur.js │ ├── CanvasRenderingContext2D.miterLimit.js │ ├── CanvasRenderingContext2D.arc.js │ ├── CanvasRenderingContext2D.putImageData.js │ ├── CanvasPattern.js │ ├── CanvasRenderingContext2D.arcTo.js │ ├── CanvasRenderingContext2D.lineWidth.js │ ├── CanvasRenderingContext2D.drawFocusIfNeeded.js │ ├── CanvasRenderingContext2D.isPointInPath.js │ ├── CanvasRenderingContext2D.createImageData.js │ ├── CanvasRenderingContext2D.currentTransform.js │ ├── CanvasRenderingContext2D.createLinearGradient.js │ ├── CanvasRenderingContext2D.ellipse.js │ ├── CanvasRenderingContext2D.fillStyle.js │ ├── CanvasRenderingContext2D.transform.js │ ├── CanvasRenderingContext2D.strokeStyle.js │ ├── CanvasRenderingContext2D.globalCompositeOperation.js │ ├── CanvasRenderingContext2D.__getClippingPath.js │ ├── CanvasRenderingContext2D.createRadialGradient.js │ ├── CanvasRenderingContext2D.drawImage.js │ ├── Path2D.js │ ├── CanvasGradient.js │ ├── CanvasRenderingContext2D.lineDash.js │ ├── CanvasRenderingContext2D.setTransform.js │ ├── CanvasRenderingContext2D.createImagePattern.js │ ├── CanvasRenderingContext2D.__getPath.js │ ├── CanvasRenderingContext2D.__getDrawCalls.js │ ├── ImageBitmap.js │ ├── ImageData.js │ └── DOMMatrix.js ├── vis.js └── mock │ └── prototype.js ├── .github ├── workflows │ └── build.yml └── FUNDING.yml ├── LICENSE ├── package.json ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md ├── types └── index.d.ts └── README.md /.prettierignore: -------------------------------------------------------------------------------- 1 | lib 2 | coverage -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | .DS_STORE 4 | coverage 5 | package-lock.json 6 | yarn.lock 7 | lib 8 | .yarn 9 | .pnp.cjs 10 | .pnp.loader.mjs 11 | -------------------------------------------------------------------------------- /src/classes/Image.js: -------------------------------------------------------------------------------- 1 | export default class Image { 2 | width = 0; 3 | height = 0; 4 | 5 | constructor(width, height) { 6 | this.width = width; 7 | this.height = height; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/env", 5 | { 6 | "targets": { 7 | "node": "8" 8 | } 9 | } 10 | ] 11 | ], 12 | "plugins": ["@babel/proposal-class-properties", "version"] 13 | } 14 | -------------------------------------------------------------------------------- /__mocks__/worker.js: -------------------------------------------------------------------------------- 1 | global.window.Worker = class { 2 | constructor(stringUrl) { 3 | this.url = stringUrl; 4 | this.onmessage = () => {}; 5 | } 6 | 7 | postMessage(msg) { 8 | this.onmessage(msg); 9 | } 10 | }; 11 | 12 | global.window.URL.createObjectURL = jest.fn(); 13 | -------------------------------------------------------------------------------- /__tests__/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * test canvas 3 | */ 4 | 5 | import { ver, setupJestCanvasMock } from '../src'; 6 | import pkg from '../package.json'; 7 | 8 | let canvas; 9 | 10 | beforeEach(() => { 11 | canvas = document.createElement('canvas'); 12 | }); 13 | 14 | describe('canvas', () => { 15 | it('should have correct version', () => { 16 | expect(ver).toBe(pkg.version); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/classes/ImageBitmap.js: -------------------------------------------------------------------------------- 1 | export default class ImageBitmap { 2 | width = 0; 3 | height = 0; 4 | 5 | _closed = false; 6 | 7 | constructor(width, height) { 8 | this.width = width; 9 | this.height = height; 10 | this.close = jest.fn(this.close.bind(this)); 11 | } 12 | 13 | close() { 14 | this.width = 0; 15 | this.height = 0; 16 | this._closed = true; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/classes/CanvasPattern.js: -------------------------------------------------------------------------------- 1 | export default class CanvasPattern { 2 | constructor() { 3 | this.setTransform = jest.fn(this.setTransform.bind(this)); 4 | } 5 | 6 | setTransform(value) { 7 | if (arguments.length > 0 && !(value instanceof Object)) 8 | throw new TypeError( 9 | "Failed to execute 'setTransform' on 'CanvasPattern': parameter 1 ('transform') is not an object." 10 | ); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hustcc 17/12/25. 3 | * Contract: i@hust.cc 4 | */ 5 | 6 | import mockWindow from './window'; 7 | 8 | // mock global window 9 | // TODO: Force coverage to ignore this branch 10 | if (typeof window !== 'undefined') { 11 | mockWindow(global.window); 12 | } 13 | 14 | export const ver = '__VERSION__'; 15 | 16 | export function setupJestCanvasMock(window) { 17 | mockWindow(window || global.window); 18 | } 19 | -------------------------------------------------------------------------------- /src/classes/TextMetrics.js: -------------------------------------------------------------------------------- 1 | export default class TextMetrics { 2 | width = 0; 3 | actualBoundingBoxLeft = 0; 4 | actualBoundingBoxRight = 0; 5 | fontBoundingBoxAscent = 0; 6 | fontBoundingBoxDescent = 0; 7 | actualBoundingBoxAscent = 0; 8 | actualBoundingBoxDescent = 0; 9 | emHeightAscent = 0; 10 | emHeightDescent = 0; 11 | hangingBaseline = 0; 12 | alphabeticBaseline = 0; 13 | ideographicBaseline = 0; 14 | 15 | constructor(text) { 16 | this.width = text.length; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.save.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('save', () => { 12 | it('should be a function', () => { 13 | expect(typeof ctx.save).toBe('function'); 14 | }); 15 | 16 | it('should be callable', () => { 17 | ctx.save(); 18 | expect(ctx.save).toBeCalled(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.restore.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('save', () => { 12 | it('should be a function', () => { 13 | expect(typeof ctx.restore).toBe('function'); 14 | }); 15 | 16 | it('should be callable', () => { 17 | ctx.restore(); 18 | expect(ctx.restore).toBeCalled(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.beginPath.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('beginPath', () => { 12 | it('should be a function', () => { 13 | expect(typeof ctx.beginPath).toBe('function'); 14 | }); 15 | 16 | it('should be callable', () => { 17 | ctx.beginPath(); 18 | expect(ctx.beginPath).toBeCalled(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.closePath.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('closePath', () => { 12 | it('should be a function', () => { 13 | expect(typeof ctx.closePath).toBe('function'); 14 | }); 15 | 16 | it('should be callable', () => { 17 | ctx.closePath(); 18 | expect(ctx.closePath).toBeCalled(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | name: Node.js ${{ matrix.node-version }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node-version: 13 | - 14 14 | - 16 15 | - 18 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: actions/setup-node@v3 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | - run: npm install 22 | - run: npm test 23 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.clearHitRegions.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('clearHitRegions', () => { 12 | it('should be a function', () => { 13 | expect(typeof ctx.clearHitRegions).toBe('function'); 14 | }); 15 | 16 | it('should be callable', () => { 17 | ctx.clearHitRegions(); 18 | expect(ctx.clearHitRegions).toBeCalled(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /__tests__/classes/__snapshots__/CanvasRenderingContext2D.__clearEvents.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`__clearEvents should clear the list of events 1`] = `[]`; 4 | 5 | exports[`__clearEvents should not prevent additional events from being collected 1`] = ` 6 | [ 7 | { 8 | "props": { 9 | "height": 4, 10 | "width": 3, 11 | "x": 1, 12 | "y": 2, 13 | }, 14 | "transform": [ 15 | 1, 16 | 0, 17 | 0, 18 | 1, 19 | 0, 20 | 0, 21 | ], 22 | "type": "fillRect", 23 | }, 24 | ] 25 | `; 26 | -------------------------------------------------------------------------------- /__tests__/classes/__snapshots__/CanvasRenderingContext2D.__clearDrawCalls.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`__clearDrawCalls should clear the list of draw calls 1`] = `[]`; 4 | 5 | exports[`__clearDrawCalls should not prevent additional draw calls from being collected 1`] = ` 6 | [ 7 | { 8 | "props": { 9 | "height": 4, 10 | "width": 3, 11 | "x": 1, 12 | "y": 2, 13 | }, 14 | "transform": [ 15 | 1, 16 | 0, 17 | 0, 18 | 1, 19 | 0, 20 | 0, 21 | ], 22 | "type": "fillRect", 23 | }, 24 | ] 25 | `; 26 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.__clearPath.js: -------------------------------------------------------------------------------- 1 | let ctx; 2 | beforeEach(() => { 3 | // get a new context each test 4 | ctx = document.createElement('canvas').getContext('2d'); 5 | }); 6 | 7 | afterEach(() => { 8 | const events = ctx.__getPath(); 9 | expect(events).toMatchSnapshot(); 10 | }); 11 | 12 | describe('__clearEvents', () => { 13 | it('should clear the list of events', () => { 14 | ctx.arc(1, 2, 3, 4, 5); 15 | ctx.__clearPath(); 16 | }); 17 | 18 | it('should not prevent additional events from being collected', () => { 19 | ctx.arc(1, 2, 3, 4, 5); 20 | ctx.__clearPath(); 21 | ctx.arc(1, 2, 3, 4, 5); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.__clearEvents.js: -------------------------------------------------------------------------------- 1 | let ctx; 2 | beforeEach(() => { 3 | // get a new context each test 4 | ctx = document.createElement('canvas').getContext('2d'); 5 | }); 6 | 7 | afterEach(() => { 8 | const events = ctx.__getEvents(); 9 | expect(events).toMatchSnapshot(); 10 | }); 11 | 12 | describe('__clearEvents', () => { 13 | it('should clear the list of events', () => { 14 | ctx.fillRect(1, 2, 3, 4); 15 | ctx.__clearEvents(); 16 | }); 17 | 18 | it('should not prevent additional events from being collected', () => { 19 | ctx.fillRect(1, 2, 3, 4); 20 | ctx.__clearEvents(); 21 | ctx.fillRect(1, 2, 3, 4); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /__tests__/vis.js: -------------------------------------------------------------------------------- 1 | /** 2 | * test vis @AntV/G2Plot 3 | */ 4 | 5 | // import { Line } from '@antv/g2plot'; 6 | 7 | describe('vis', () => { 8 | it('vis can pass', () => { 9 | // const div = document.createElement('canvas'); 10 | // const line = new Line(div, { 11 | // data: [ 12 | // { x: 'A', y: 10 }, 13 | // { x: 'B', y: 20 }, 14 | // { x: 'C', y: 30 }, 15 | // ], 16 | // xField: 'x', 17 | // yField: 'y', 18 | // }); 19 | // line.render(); 20 | // expect(line.container.querySelector('canvas')).not.toBe(null); 21 | // line.destroy(); 22 | // expect(line.container.querySelector('canvas')).toBe(null); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.__clearDrawCalls.js: -------------------------------------------------------------------------------- 1 | let ctx; 2 | beforeEach(() => { 3 | // get a new context each test 4 | ctx = document.createElement('canvas').getContext('2d'); 5 | }); 6 | 7 | afterEach(() => { 8 | const drawCalls = ctx.__getDrawCalls(); 9 | expect(drawCalls).toMatchSnapshot(); 10 | }); 11 | 12 | describe('__clearDrawCalls', () => { 13 | it('should clear the list of draw calls', () => { 14 | ctx.fillRect(1, 2, 3, 4); 15 | ctx.__clearDrawCalls(); 16 | }); 17 | 18 | it('should not prevent additional draw calls from being collected', () => { 19 | ctx.fillRect(1, 2, 3, 4); 20 | ctx.__clearDrawCalls(); 21 | ctx.fillRect(1, 2, 3, 4); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.removeHitRegion.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('removeHitRegion', () => { 12 | it('should be a function', () => { 13 | expect(typeof ctx.removeHitRegion).toBe('function'); 14 | }); 15 | 16 | it('should be callable', () => { 17 | ctx.removeHitRegion('test'); 18 | expect(ctx.removeHitRegion).toBeCalled(); 19 | }); 20 | 21 | it('should throw if less than 1 parameter is given', () => { 22 | expect(() => ctx.removeHitRegion()).toThrow(TypeError); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.scrollPathIntoView.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('scrollPathIntoView', () => { 12 | it('should be a function', () => { 13 | expect(typeof ctx.scrollPathIntoView).toBe('function'); 14 | }); 15 | 16 | it('should be callable', () => { 17 | ctx.scrollPathIntoView(); 18 | expect(ctx.scrollPathIntoView).toBeCalled(); 19 | }); 20 | 21 | it('should accept a path object', () => { 22 | expect(() => ctx.scrollPathIntoView(new Path2D())).not.toThrow(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.lineTo.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('lineTo', () => { 12 | it('should be a function', () => { 13 | expect(typeof ctx.lineTo).toBe('function'); 14 | }); 15 | 16 | it('should be callable', () => { 17 | ctx.lineTo(1, 2, 3, 4); 18 | expect(ctx.lineTo).toBeCalled(); 19 | }); 20 | 21 | it('should throw if less than 2 parameters are given', () => { 22 | expect(() => ctx.lineTo()).toThrow(TypeError); 23 | expect(() => ctx.lineTo(1)).toThrow(TypeError); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.moveTo.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('moveTo', () => { 12 | it('should be a function', () => { 13 | expect(typeof ctx.moveTo).toBe('function'); 14 | }); 15 | 16 | it('should be callable', () => { 17 | ctx.moveTo(1, 2, 3, 4); 18 | expect(ctx.moveTo).toBeCalled(); 19 | }); 20 | 21 | it('should throw if less than 2 parameters are given', () => { 22 | expect(() => ctx.moveTo()).toThrow(TypeError); 23 | expect(() => ctx.moveTo(1)).toThrow(TypeError); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: ['https://paypal.me/hustcc', 'https://atool.vip'] 13 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.fillText.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('fillText', () => { 12 | it('should be a function', () => { 13 | expect(typeof ctx.fillText).toBe('function'); 14 | }); 15 | 16 | it('should be callable', () => { 17 | ctx.fillText('hello world!', 1, 2); 18 | expect(ctx.fillText).toBeCalled(); 19 | }); 20 | 21 | it('should throw if argument length < 3', () => { 22 | expect(() => ctx.fillText()).toThrow(); 23 | expect(() => ctx.fillText(1)).toThrow(); 24 | expect(() => ctx.fillText(1, 2)).toThrow(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/mock/createCanvasEvent.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This function returns a CanvasRenderingContext2DEvent. Whenever an operation would modify the canvas 3 | * context, this function should be used to generate an "event" that represents that sort of modification. 4 | * This will be used to mock for snapshots. 5 | * 6 | * @example 7 | * interface CanvasRenderingContext2DEvent { 8 | * type: string; 9 | * transform: [number, number, number, number, number, number]; // the resulting current transform 10 | * props: { 11 | * // if the event is a property was set, `event.props.value` would be set 12 | * [propName: string]: any; 13 | * }; 14 | * } 15 | */ 16 | export default function createCanvasEvent(type, transform, props) { 17 | return { type, transform, props }; 18 | } 19 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.strokeText.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('strokeText', () => { 12 | it('should be a function', () => { 13 | expect(typeof ctx.strokeText).toBe('function'); 14 | }); 15 | 16 | it('should be callable', () => { 17 | ctx.strokeText('hello world!', 1, 2); 18 | expect(ctx.strokeText).toBeCalled(); 19 | }); 20 | 21 | it('should throw if argument length < 3', () => { 22 | expect(() => ctx.strokeText()).toThrow(); 23 | expect(() => ctx.strokeText(1)).toThrow(); 24 | expect(() => ctx.strokeText(1, 2)).toThrow(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.font.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('font', () => { 12 | it('should not accept invalid fonts', () => { 13 | ctx.font = 'invalid'; 14 | expect(ctx.font).toBe('10px sans-serif'); 15 | }); 16 | 17 | it('should accept valid fonts', () => { 18 | ctx.font = '12pt Times New Roman'; 19 | expect(ctx.font).toBe('16px "Times New Roman"'); 20 | }); 21 | 22 | it('should save and restore font values', () => { 23 | ctx.save(); 24 | ctx.font = '12pt Times New Roman'; 25 | ctx.restore(); 26 | expect(ctx.font).toBe('10px sans-serif'); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.rect.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('rect', () => { 12 | it('should be a function', () => { 13 | expect(typeof ctx.rect).toBe('function'); 14 | }); 15 | 16 | it('should be callable', () => { 17 | ctx.rect(1, 2, 3, 4); 18 | expect(ctx.rect).toBeCalled(); 19 | }); 20 | 21 | it('should throw if less than 4 parameters are given', () => { 22 | expect(() => ctx.rect()).toThrow(TypeError); 23 | expect(() => ctx.rect(1)).toThrow(TypeError); 24 | expect(() => ctx.rect(1, 2)).toThrow(TypeError); 25 | expect(() => ctx.rect(1, 2, 3)).toThrow(TypeError); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('CanvasRenderingContext2D prototype', () => { 12 | it('should be instanceof CanvasRenderingContext2D', () => { 13 | expect(ctx).toBeInstanceOf(CanvasRenderingContext2D); 14 | }); 15 | 16 | it('should have a canvas property', () => { 17 | expect(ctx.canvas).toBe(canvas); 18 | }); 19 | 20 | it('should have a getContext function', () => { 21 | expect(canvas.getContext).toBeInstanceOf(Function); 22 | }); 23 | 24 | it('should be defined on the prototype', () => { 25 | expect(HTMLCanvasElement.prototype.getContext).toBeInstanceOf(Function); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.resetTransform.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('resetTransform', () => { 12 | it('should be a function', () => { 13 | expect(typeof ctx.resetTransform).toBe('function'); 14 | }); 15 | 16 | it('should be callable', () => { 17 | ctx.resetTransform(); 18 | expect(ctx.resetTransform).toBeCalled(); 19 | }); 20 | 21 | it('should reset the transform', () => { 22 | ctx.setTransform(1, 2, 3, 4, 5, 6); 23 | expect(ctx.currentTransform).toEqual(new DOMMatrix([1, 2, 3, 4, 5, 6])); 24 | ctx.resetTransform(); 25 | expect(ctx.currentTransform).toEqual(new DOMMatrix([1, 0, 0, 1, 0, 0])); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.fillRect.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('fillRect', () => { 12 | it('should be a function', () => { 13 | expect(typeof ctx.fillRect).toBe('function'); 14 | }); 15 | 16 | it('should be callable', () => { 17 | ctx.fillRect(1, 2, 3, 4); 18 | expect(ctx.fillRect).toBeCalled(); 19 | }); 20 | 21 | it('should throw if less than 4 parameters are given', () => { 22 | expect(() => ctx.fillRect()).toThrow(TypeError); 23 | expect(() => ctx.fillRect(1)).toThrow(TypeError); 24 | expect(() => ctx.fillRect(1, 2)).toThrow(TypeError); 25 | expect(() => ctx.fillRect(1, 2, 3)).toThrow(TypeError); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.clearRect.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('clearRect', () => { 12 | it('should be a function', () => { 13 | expect(typeof ctx.clearRect).toBe('function'); 14 | }); 15 | 16 | it('should be callable', () => { 17 | ctx.clearRect(1, 2, 3, 4); 18 | expect(ctx.clearRect).toBeCalled(); 19 | }); 20 | 21 | it('should throw if less than 4 parameters are given', () => { 22 | expect(() => ctx.clearRect()).toThrow(TypeError); 23 | expect(() => ctx.clearRect(1)).toThrow(TypeError); 24 | expect(() => ctx.clearRect(1, 2)).toThrow(TypeError); 25 | expect(() => ctx.clearRect(1, 2, 3)).toThrow(TypeError); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.strokeRect.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('strokeRect', () => { 12 | it('should be a function', () => { 13 | expect(typeof ctx.strokeRect).toBe('function'); 14 | }); 15 | 16 | it('should be callable', () => { 17 | ctx.strokeRect(1, 2, 3, 4); 18 | expect(ctx.strokeRect).toBeCalled(); 19 | }); 20 | 21 | it('should throw if less than 4 parameters are given', () => { 22 | expect(() => ctx.strokeRect()).toThrow(TypeError); 23 | expect(() => ctx.strokeRect(1)).toThrow(TypeError); 24 | expect(() => ctx.strokeRect(1, 2)).toThrow(TypeError); 25 | expect(() => ctx.strokeRect(1, 2, 3)).toThrow(TypeError); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.imageSmoothingEnabled.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('imageSmoothingQuality', () => { 12 | it('should set the imageSmoothingEnabled values to truthy values', () => { 13 | [true, false, 1, 0, null, '', Infinity, void 0, NaN].forEach((e) => { 14 | ctx.imageSmoothingEnabled = e; 15 | expect(ctx.imageSmoothingEnabled).toBe(!!e); 16 | }); 17 | }); 18 | 19 | it('should save and restore to modify the imageSmoothingEnabled values', () => { 20 | ctx.save(); 21 | ctx.imageSmoothingEnabled = false; 22 | expect(ctx.imageSmoothingEnabled).toBeFalsy(); 23 | ctx.restore(); 24 | expect(ctx.imageSmoothingEnabled).toBeTruthy(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/classes/CanvasGradient.js: -------------------------------------------------------------------------------- 1 | import { MooColor } from 'moo-color'; 2 | 3 | export default class CanvasGradient { 4 | constructor() { 5 | this.addColorStop = jest.fn(this.addColorStop.bind(this)); 6 | } 7 | 8 | addColorStop(offset, color) { 9 | const numoffset = Number(offset); 10 | if (!Number.isFinite(numoffset) || numoffset < 0 || numoffset > 1) { 11 | throw new DOMException( 12 | 'IndexSizeError', 13 | "Failed to execute 'addColorStop' on 'CanvasGradient': The provided value ('" + 14 | numoffset + 15 | "') is outside the range (0.0, 1.0)" 16 | ); 17 | } 18 | try { 19 | new MooColor(color); 20 | } catch (e) { 21 | throw new SyntaxError( 22 | "Failed to execute 'addColorStop' on 'CanvasGradient': The value provided ('" + 23 | color + 24 | "') could not be parsed as a color." 25 | ); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.quadraticCurveTo.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('quadraticCurveTo', () => { 12 | it('should be a function', () => { 13 | expect(typeof ctx.quadraticCurveTo).toBe('function'); 14 | }); 15 | 16 | it('should be callable', () => { 17 | ctx.quadraticCurveTo(1, 2, 3, 4); 18 | expect(ctx.quadraticCurveTo).toBeCalled(); 19 | }); 20 | 21 | it('should throw if less than 4 parameters are given', () => { 22 | expect(() => ctx.quadraticCurveTo()).toThrow(TypeError); 23 | expect(() => ctx.quadraticCurveTo(1)).toThrow(TypeError); 24 | expect(() => ctx.quadraticCurveTo(1, 2)).toThrow(TypeError); 25 | expect(() => ctx.quadraticCurveTo(1, 2, 3)).toThrow(TypeError); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.stroke.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | ctx.setTransform(1, 2, 3, 4, 5, 6); 10 | }); 11 | 12 | describe('stroke', () => { 13 | it('should be a function', () => { 14 | expect(typeof ctx.stroke).toBe('function'); 15 | }); 16 | 17 | it('should be callable', () => { 18 | ctx.stroke(); 19 | expect(ctx.stroke).toBeCalled(); 20 | }); 21 | 22 | it('should accept a path parameter', () => { 23 | expect(() => ctx.stroke(new Path2D())).not.toThrow(); 24 | }); 25 | 26 | it('should throw if an arguments is passed to it and it is not a Path2D', () => { 27 | [1, Infinity, 'test', null, void 0, [], {}].forEach((e) => { 28 | expect(() => ctx.stroke(e)).toThrow(TypeError); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.getImageData.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('getImageData', () => { 12 | it('should be a function', () => { 13 | expect(typeof ctx.getImageData).toBe('function'); 14 | }); 15 | 16 | it('should be callable', () => { 17 | ctx.getImageData(); 18 | expect(ctx.getImageData).toBeCalled(); 19 | }); 20 | 21 | it('should return a image data from getImageData', () => { 22 | expect(ctx.getImageData()).toBeInstanceOf(ImageData); 23 | }); 24 | 25 | it('should return a image data from getImageData of proper size', () => { 26 | const data = ctx.getImageData(); 27 | expect(data.width).toBe(400); 28 | expect(data.height).toBe(300); 29 | expect(data.data.length).toBe(400 * 300 * 4); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.lineCap.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('lineCap', () => { 12 | it('should accept valid lineCap values', () => { 13 | ['butt', 'round', 'square'].forEach((e) => { 14 | ctx.lineCap = e; 15 | expect(ctx.lineCap).toBe(e); 16 | }); 17 | }); 18 | 19 | it('should ignore invalid lineCap values', () => { 20 | [true, false, 1, 0, null, '', Infinity, void 0, NaN, 'invalid!'].forEach( 21 | (e) => { 22 | ctx.lineCap = e; 23 | expect(ctx.lineCap).toBe('butt'); 24 | } 25 | ); 26 | }); 27 | 28 | it('should save and restore lineCap values', () => { 29 | ctx.save(); 30 | ctx.lineCap = 'round'; 31 | ctx.restore(); 32 | expect(ctx.lineCap).toBe('butt'); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.isPointInStroke.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | const p = new Path2D(); 4 | 5 | beforeEach(() => { 6 | canvas = document.createElement('canvas'); 7 | ctx = canvas.getContext('2d'); 8 | canvas.width = 400; 9 | canvas.height = 300; 10 | }); 11 | 12 | describe('isPointInStroke', () => { 13 | it('should be a function', () => { 14 | expect(typeof ctx.isPointInStroke).toBe('function'); 15 | }); 16 | 17 | it('should be callable', () => { 18 | ctx.isPointInStroke(1, 2); 19 | expect(ctx.isPointInStroke).toBeCalled(); 20 | }); 21 | 22 | it('should throw if less than 2 arguments are provided', () => { 23 | expect(() => ctx.isPointInStroke(1)).toThrow(TypeError); 24 | expect(() => ctx.isPointInStroke()).toThrow(TypeError); 25 | }); 26 | 27 | it('should always return false', () => { 28 | expect(ctx.isPointInStroke(1, 2)).toBeFalsy(); 29 | expect(ctx.isPointInStroke(p, 1, 2)).toBeFalsy(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.filter.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('filter', () => { 12 | it("should have a filter property default value 'none'", () => { 13 | expect(ctx.filter).toBe('none'); 14 | }); 15 | 16 | it('should set the filter property', () => { 17 | ctx.filter = 'sepia(100%)'; 18 | expect(ctx.filter).toBe('sepia(100%)'); 19 | }); 20 | 21 | it('should ignore non-string values', () => { 22 | ctx.filter = 2; 23 | expect(ctx.filter).toBe('none'); 24 | }); 25 | 26 | it('should set empty string to none', () => { 27 | ctx.filter = ''; 28 | expect(ctx.filter).toBe('none'); 29 | }); 30 | 31 | it('should save and restore filter values', () => { 32 | ctx.save(); 33 | ctx.filter = 'sepia(100%)'; 34 | ctx.restore(); 35 | expect(ctx.filter).toBe('none'); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /__tests__/classes/__snapshots__/CanvasRenderingContext2D.__clearPath.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`__clearEvents should clear the list of events 1`] = ` 4 | [ 5 | { 6 | "props": {}, 7 | "transform": [ 8 | 1, 9 | 0, 10 | 0, 11 | 1, 12 | 0, 13 | 0, 14 | ], 15 | "type": "beginPath", 16 | }, 17 | ] 18 | `; 19 | 20 | exports[`__clearEvents should not prevent additional events from being collected 1`] = ` 21 | [ 22 | { 23 | "props": {}, 24 | "transform": [ 25 | 1, 26 | 0, 27 | 0, 28 | 1, 29 | 0, 30 | 0, 31 | ], 32 | "type": "beginPath", 33 | }, 34 | { 35 | "props": { 36 | "anticlockwise": false, 37 | "endAngle": 5, 38 | "radius": 3, 39 | "startAngle": 4, 40 | "x": 1, 41 | "y": 2, 42 | }, 43 | "transform": [ 44 | 1, 45 | 0, 46 | 0, 47 | 1, 48 | 0, 49 | 0, 50 | ], 51 | "type": "arc", 52 | }, 53 | ] 54 | `; 55 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.bezierCurveTo.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('bezierCurveTo', () => { 12 | it('should be a function', () => { 13 | expect(typeof ctx.bezierCurveTo).toBe('function'); 14 | }); 15 | 16 | it('should be callable', () => { 17 | ctx.bezierCurveTo(1, 2, 3, 4, 5, 6); 18 | expect(ctx.bezierCurveTo).toBeCalled(); 19 | }); 20 | 21 | it('should throw if less than 6 parameters are given', () => { 22 | expect(() => ctx.bezierCurveTo()).toThrow(TypeError); 23 | expect(() => ctx.bezierCurveTo(1)).toThrow(TypeError); 24 | expect(() => ctx.bezierCurveTo(1, 2)).toThrow(TypeError); 25 | expect(() => ctx.bezierCurveTo(1, 2, 3)).toThrow(TypeError); 26 | expect(() => ctx.bezierCurveTo(1, 2, 3, 4)).toThrow(TypeError); 27 | expect(() => ctx.bezierCurveTo(1, 2, 3, 4, 5)).toThrow(TypeError); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.globalAlpha.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('globalAlpha', () => { 12 | it('should ignore non finite globalAlpha values', () => { 13 | [Infinity, -Infinity, void 0, NaN].forEach((e) => { 14 | ctx.globalAlpha = e; 15 | expect(ctx.globalAlpha).toBe(1); 16 | }); 17 | }); 18 | 19 | it('should ignore out of range values', () => { 20 | [-1, 1.1].forEach((e) => { 21 | ctx.globalAlpha = e; 22 | expect(ctx.globalAlpha).toBe(1); 23 | }); 24 | }); 25 | 26 | it('should not ignore globalAlpha values that are within range', () => { 27 | [0.1, 0.2, 0.3, 0.4].forEach((e) => { 28 | ctx.globalAlpha = e; 29 | expect(ctx.globalAlpha).toBe(e); 30 | }); 31 | }); 32 | 33 | it('should be 0 when globalAlpha is set to null', () => { 34 | ctx.globalAlpha = null; 35 | expect(ctx.globalAlpha).toBe(0); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.imageSmoothingQuality.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('imageSmoothingQuality', () => { 12 | it('should accept valid imageSmoothingQuality values', () => { 13 | ['high', 'medium', 'low'].forEach((e) => { 14 | ctx.imageSmoothingQuality = e; 15 | expect(ctx.imageSmoothingQuality).toBe(e); 16 | }); 17 | }); 18 | 19 | it('should ignore invalid imageSmoothingQuality values', () => { 20 | [true, false, 1, 0, null, '', Infinity, void 0, NaN, 'invalid!'].forEach( 21 | (e) => { 22 | ctx.imageSmoothingQuality = e; 23 | expect(ctx.imageSmoothingQuality).toBe('low'); 24 | } 25 | ); 26 | }); 27 | 28 | it('should save and restore imageSmoothingQuality values', () => { 29 | ctx.save(); 30 | ctx.imageSmoothingQuality = 'high'; 31 | ctx.restore(); 32 | expect(ctx.imageSmoothingQuality).toBe('low'); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.textAlign.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('textAlign', () => { 12 | it("should set the default value to 'start'", () => { 13 | expect(ctx.textAlign).toBe('start'); 14 | }); 15 | 16 | it("should not set the value if it's not a valid textAlign", () => { 17 | ctx.textAlign = 'wrong!'; 18 | expect(ctx.textAlign).toBe('start'); 19 | }); 20 | 21 | it("should set the textAlign if it's a valid textAlign", () => { 22 | ['left', 'right', 'center', 'start', 'end'].forEach((e) => { 23 | ctx.textAlign = e; 24 | expect(ctx.textAlign).toBe(e); 25 | }); 26 | }); 27 | 28 | it('should save and restore textAlign values', () => { 29 | ctx.textAlign = 'right'; 30 | ctx.save(); 31 | ctx.textAlign = 'center'; 32 | expect(ctx.textAlign).toBe('center'); 33 | ctx.restore(); 34 | expect(ctx.textAlign).toBe('right'); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /__tests__/classes/TextMetrics.js: -------------------------------------------------------------------------------- 1 | const props = [ 2 | 'width', 3 | 'actualBoundingBoxLeft', 4 | 'actualBoundingBoxRight', 5 | 'fontBoundingBoxAscent', 6 | 'fontBoundingBoxDescent', 7 | 'actualBoundingBoxAscent', 8 | 'actualBoundingBoxDescent', 9 | 'emHeightAscent', 10 | 'emHeightDescent', 11 | 'hangingBaseline', 12 | 'alphabeticBaseline', 13 | 'ideographicBaseline', 14 | ]; 15 | 16 | describe('TextMetrics', () => { 17 | it('should return a text metrics object', () => { 18 | const m = new TextMetrics('test'); 19 | expect(m).toBeInstanceOf(TextMetrics); 20 | }); 21 | 22 | it('should have a width of text length for testing purposes', () => { 23 | ['one', 'two1', 'three', undefined, 1, null, 102].forEach((val) => { 24 | val = String(val); 25 | const m = new TextMetrics(val); 26 | expect(m.width).toBe(val.length); 27 | }); 28 | }); 29 | 30 | it('should have every property defined in the specification', () => { 31 | const m = new TextMetrics(''); 32 | props.forEach((val) => { 33 | expect(m[val]).toBe(0); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.shadowOffsetX.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('shadowOffsetX', () => { 12 | it('should have a default value', () => { 13 | expect(ctx.shadowOffsetX).toBe(0); 14 | }); 15 | 16 | it('should ignore non finite values', () => { 17 | [Infinity, -Infinity, null, void 0, NaN].forEach((e) => { 18 | ctx.shadowOffsetX = e; 19 | expect(ctx.shadowOffsetX).toBe(0); 20 | }); 21 | }); 22 | 23 | it('should not ignore values that are within range', () => { 24 | [1, 10, '30', '10.2', '-3'].forEach((e) => { 25 | ctx.shadowOffsetX = e; 26 | expect(ctx.shadowOffsetX).toBe(Number(e)); 27 | }); 28 | }); 29 | 30 | it('should save and restore values', () => { 31 | ctx.shadowOffsetX = 2; 32 | ctx.save(); 33 | ctx.shadowOffsetX = 10; 34 | expect(ctx.shadowOffsetX).toBe(10); 35 | ctx.restore(); 36 | expect(ctx.shadowOffsetX).toBe(2); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.shadowOffsetY.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('shadowOffsetY', () => { 12 | it('should have a default value', () => { 13 | expect(ctx.shadowOffsetY).toBe(0); 14 | }); 15 | 16 | it('should ignore non finite values', () => { 17 | [Infinity, -Infinity, null, void 0, NaN].forEach((e) => { 18 | ctx.shadowOffsetY = e; 19 | expect(ctx.shadowOffsetY).toBe(0); 20 | }); 21 | }); 22 | 23 | it('should not ignore values that are within range', () => { 24 | [1, 10, '30', '10.2', '-3'].forEach((e) => { 25 | ctx.shadowOffsetY = e; 26 | expect(ctx.shadowOffsetY).toBe(Number(e)); 27 | }); 28 | }); 29 | 30 | it('should save and restore values', () => { 31 | ctx.shadowOffsetY = 2; 32 | ctx.save(); 33 | ctx.shadowOffsetY = 10; 34 | expect(ctx.shadowOffsetY).toBe(10); 35 | ctx.restore(); 36 | expect(ctx.shadowOffsetY).toBe(2); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Adamfsk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.clip.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | const p = new Path2D(); 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('clip', () => { 12 | it('should be a function', () => { 13 | expect(typeof ctx.clip).toBe('function'); 14 | }); 15 | 16 | it('should be callable', () => { 17 | ctx.clip(); 18 | expect(ctx.clip).toBeCalled(); 19 | }); 20 | 21 | it('should clip paths', () => { 22 | expect(() => ctx.clip(p)).not.toThrow(); 23 | }); 24 | 25 | it('should throw if clipRule is not valid clipRule', () => { 26 | [null, 1, Infinity, NaN, void 0, 'bad!'].forEach((e) => { 27 | expect(() => ctx.clip(p, e)).toThrow(TypeError); 28 | expect(() => ctx.clip(e)).toThrow(TypeError); 29 | }); 30 | }); 31 | 32 | it('should accept valid fillRules', () => { 33 | ['evenodd', 'nonzero'].forEach((e) => { 34 | expect(() => ctx.clip(e)).not.toThrow(); 35 | expect(() => ctx.clip(p, e)).not.toThrow(); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.fill.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | const p = new Path2D(); 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('fill', () => { 12 | it('should be a function', () => { 13 | expect(typeof ctx.fill).toBe('function'); 14 | }); 15 | 16 | it('should be callable', () => { 17 | ctx.fill(); 18 | expect(ctx.fill).toBeCalled(); 19 | }); 20 | 21 | it('should fill paths', () => { 22 | expect(() => ctx.fill(p)).not.toThrow(); 23 | }); 24 | 25 | it('should throw if fillRule is not valid fillRule', () => { 26 | [null, 1, Infinity, NaN, void 0, 'bad!'].forEach((e) => { 27 | expect(() => ctx.fill(p, e)).toThrow(TypeError); 28 | expect(() => ctx.fill(e)).toThrow(TypeError); 29 | }); 30 | }); 31 | 32 | it('should accept valid fillRules', () => { 33 | ['evenodd', 'nonzero'].forEach((e) => { 34 | expect(() => ctx.fill(e)).not.toThrow(); 35 | expect(() => ctx.fill(p, e)).not.toThrow(); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.rotate.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | ctx.setTransform(1, 2, 3, 4, 5, 6); 10 | }); 11 | 12 | describe('rotate', () => { 13 | it('should be a function', () => { 14 | expect(typeof ctx.rotate).toBe('function'); 15 | }); 16 | 17 | it('should be callable', () => { 18 | ctx.rotate(1); 19 | expect(ctx.rotate).toBeCalled(); 20 | }); 21 | 22 | it('should rotate the current transform', () => { 23 | ctx.rotate(Math.PI); 24 | expect(ctx.currentTransform).toEqual( 25 | new DOMMatrix([-0.9999999999999997, -1.9999999999999996, -3, -4, 5, 6]) 26 | ); 27 | }); 28 | 29 | it('should throw if argument count is less than 2', () => { 30 | expect(() => ctx.rotate()).toThrow(TypeError); 31 | }); 32 | 33 | it("shouldn't rotate the transform if any of the values cannot be coerced into finite numbers", () => { 34 | ctx.rotate(NaN); 35 | expect(ctx.currentTransform).toEqual(new DOMMatrix([1, 2, 3, 4, 5, 6])); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.lineJoin.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('lineJoin', () => { 12 | it("should set the default value lineJoin to 'miter'", () => { 13 | expect(ctx.lineJoin).toBe('miter'); 14 | }); 15 | 16 | it("should set the lineJoin if it's a valid lineJoin", () => { 17 | ctx.lineJoin = 'wrong!'; 18 | expect(ctx.lineJoin).toBe('miter'); 19 | }); 20 | 21 | it("should set the lineJoin if it's a valid lineJoin", () => { 22 | ctx.lineJoin = 'round'; 23 | expect(ctx.lineJoin).toBe('round'); 24 | ctx.lineJoin = 'bevel'; 25 | expect(ctx.lineJoin).toBe('bevel'); 26 | ctx.lineJoin = 'miter'; 27 | expect(ctx.lineJoin).toBe('miter'); 28 | }); 29 | 30 | it('should save and restore lineJoin values', () => { 31 | ctx.lineJoin = 'round'; 32 | ctx.save(); 33 | ctx.lineJoin = 'bevel'; 34 | expect(ctx.lineJoin).toBe('bevel'); 35 | ctx.restore(); 36 | expect(ctx.lineJoin).toBe('round'); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.direction.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('direction', () => { 12 | it("should set the default value direction to 'inherit'", () => { 13 | expect(ctx.direction).toBe('inherit'); 14 | }); 15 | 16 | it("should set the direction if it's a valid direction", () => { 17 | ctx.direction = 'wrong!'; 18 | expect(ctx.direction).toBe('inherit'); 19 | }); 20 | 21 | it("should set the direction if it's a valid direction", () => { 22 | ctx.direction = 'ltr'; 23 | expect(ctx.direction).toBe('ltr'); 24 | ctx.direction = 'rtl'; 25 | expect(ctx.direction).toBe('rtl'); 26 | ctx.direction = 'inherit'; 27 | expect(ctx.direction).toBe('inherit'); 28 | }); 29 | 30 | it('should save and restore direction values', () => { 31 | ctx.direction = 'ltr'; 32 | ctx.save(); 33 | ctx.direction = 'rtl'; 34 | expect(ctx.direction).toBe('rtl'); 35 | ctx.restore(); 36 | expect(ctx.direction).toBe('ltr'); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.lineDashOffset.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('lineDashOffset', () => { 12 | it('should have a default value of 0', () => { 13 | expect(ctx.lineDashOffset).toBe(0); 14 | }); 15 | 16 | it('should cast js values to numbers and ignore non-finite values when setting the lineDashOffset property', () => { 17 | [0, 10, -Infinity, 'null', null, NaN].forEach((e) => { 18 | ctx.lineDashOffset = 0; 19 | ctx.lineDashOffset = e; 20 | const cast = Number(e); 21 | if (Number.isFinite(cast)) { 22 | expect(ctx.lineDashOffset).toBe(cast); 23 | } else { 24 | expect(ctx.lineDashOffset).toBe(0); 25 | } 26 | }); 27 | }); 28 | 29 | it('should save and restore lineDashOffset values when calling save() and restore()', () => { 30 | ctx.save(); 31 | ctx.lineDashOffset = 30; 32 | expect(ctx.lineDashOffset).toBe(30); 33 | ctx.restore(); 34 | expect(ctx.lineDashOffset).toBe(0); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.addHitRegion.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('addHitRegion', () => { 12 | it('should be a function', () => { 13 | expect(ctx.addHitRegion).toBeTruthy(); 14 | }); 15 | 16 | it('should be callable', () => { 17 | ctx.addHitRegion({ id: 'test' }); 18 | expect(ctx.addHitRegion).toBeCalled(); 19 | }); 20 | 21 | it('should throw if called with no parameters', () => { 22 | expect(() => ctx.addHitRegion()).toThrow(DOMException); 23 | }); 24 | 25 | it("should throw if fillRule is set and isn't 'evenodd' or 'nonzero'", () => { 26 | expect(() => 27 | ctx.addHitRegion({ id: 'test', fillRule: 'wrong!' }) 28 | ).toThrow(); 29 | }); 30 | 31 | it('should not throw if fillRule is valid', () => { 32 | expect(() => 33 | ctx.addHitRegion({ id: 'test', fillRule: 'evenodd' }) 34 | ).not.toThrow(); 35 | expect(() => 36 | ctx.addHitRegion({ id: 'test', fillRule: 'nonzero' }) 37 | ).not.toThrow(); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.textBaseline.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('textBaseline', () => { 12 | it("should set the default value to 'alphabetic'", () => { 13 | expect(ctx.textBaseline).toBe('alphabetic'); 14 | }); 15 | 16 | it("shouldn't set the value if it's not a valid textBaseline", () => { 17 | ctx.textBaseline = 'wrong!'; 18 | expect(ctx.textBaseline).toBe('alphabetic'); 19 | }); 20 | 21 | it("should set the textBaseline if it's a valid textBaseline", () => { 22 | ['top', 'hanging', 'middle', 'alphabetic', 'ideographic', 'bottom'].forEach( 23 | (e) => { 24 | ctx.textBaseline = e; 25 | expect(ctx.textBaseline).toBe(e); 26 | } 27 | ); 28 | }); 29 | 30 | it('should save and restore textBaseline values', () => { 31 | ctx.textBaseline = 'top'; 32 | ctx.save(); 33 | ctx.textBaseline = 'hanging'; 34 | expect(ctx.textBaseline).toBe('hanging'); 35 | ctx.restore(); 36 | expect(ctx.textBaseline).toBe('top'); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.scale.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | ctx.setTransform(1, 2, 3, 4, 5, 6); 10 | }); 11 | 12 | describe('scale', () => { 13 | it('should be a function', () => { 14 | expect(typeof ctx.scale).toBe('function'); 15 | }); 16 | 17 | it('should be callable', () => { 18 | ctx.scale(1, 2); 19 | expect(ctx.scale).toBeCalled(); 20 | }); 21 | 22 | it('should scale the current transform', () => { 23 | ctx.scale(2, 3); 24 | expect(ctx.currentTransform).toEqual(new DOMMatrix([2, 4, 9, 12, 5, 6])); 25 | }); 26 | 27 | it('should throw if argument count is less than 2', () => { 28 | expect(() => ctx.scale(1)).toThrow(TypeError); 29 | }); 30 | 31 | it('should not scale the transform if any of the values cannot be coerced into finite numbers', () => { 32 | ctx.scale(1, Infinity); 33 | expect(ctx.currentTransform).toEqual(new DOMMatrix([1, 2, 3, 4, 5, 6])); 34 | 35 | ctx.scale(NaN, 2); 36 | expect(ctx.currentTransform).toEqual(new DOMMatrix([1, 2, 3, 4, 5, 6])); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.translate.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('translate', () => { 12 | it('should be a function', () => { 13 | expect(typeof ctx.translate).toBe('function'); 14 | }); 15 | 16 | it('should be callable', () => { 17 | ctx.translate(1, 2); 18 | expect(ctx.translate).toBeCalled(); 19 | }); 20 | 21 | it('should translate the current transform', () => { 22 | ctx.translate(2, 3); 23 | expect(ctx.currentTransform).toEqual(new DOMMatrix([1, 0, 0, 1, 2, 3])); 24 | }); 25 | 26 | it('should throw if argument count is less than 2', () => { 27 | expect(() => ctx.translate(1)).toThrow(TypeError); 28 | }); 29 | 30 | it('should not translate the transform if any of the values cannot be coerced into finite numbers', () => { 31 | ctx.translate(1, Infinity); 32 | expect(ctx.currentTransform).toEqual(new DOMMatrix([1, 0, 0, 1, 0, 0])); 33 | 34 | ctx.translate(NaN, 2); 35 | expect(ctx.currentTransform).toEqual(new DOMMatrix([1, 0, 0, 1, 0, 0])); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.measureText.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('measureText', () => { 12 | it('should be a function', () => { 13 | expect(typeof ctx.measureText).toBe('function'); 14 | }); 15 | 16 | it('should be callable', () => { 17 | ctx.measureText(1); 18 | expect(ctx.measureText).toBeCalled(); 19 | }); 20 | 21 | it('should return a TextMetrics object', () => { 22 | const m = ctx.measureText('Hello There!'); 23 | expect(m).toBeInstanceOf(TextMetrics); 24 | }); 25 | 26 | it('should return a text metrics object of expected width', () => { 27 | const me = ctx.measureText('Test!'); 28 | expect(me.width).toBe(5); 29 | }); 30 | 31 | it('should return a text metrics object for different kinds of input', () => { 32 | [NaN, 1, null, void 0, 'bleh'].forEach((val) => { 33 | expect(() => ctx.measureText(val)).not.toThrow(); 34 | }); 35 | }); 36 | 37 | it('should throw if an argument is not provided', () => { 38 | expect(() => ctx.measureText()).toThrow(); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /src/classes/Path2D.js: -------------------------------------------------------------------------------- 1 | import CanvasRenderingContext2D from './CanvasRenderingContext2D'; 2 | 3 | // Path2D.prototype 4 | const Path2DFunc = ['addPath']; 5 | 6 | const borrowedFromCanvas = [ 7 | 'closePath', 8 | 'moveTo', 9 | 'lineTo', 10 | 'bezierCurveTo', 11 | 'quadraticCurveTo', 12 | 'arc', 13 | 'arcTo', 14 | 'ellipse', 15 | 'rect', 16 | ]; 17 | 18 | export default class Path2D { 19 | _path = []; 20 | _events = []; 21 | _stackIndex = 0; 22 | _transformStack = [[1, 0, 0, 1, 0, 0]]; 23 | 24 | constructor() { 25 | borrowedFromCanvas.forEach((key) => { 26 | this[key] = jest.fn(CanvasRenderingContext2D.prototype[key].bind(this)); 27 | }); 28 | Path2DFunc.forEach((key) => { 29 | this[key] = jest.fn(this[key].bind(this)); 30 | }); 31 | } 32 | 33 | addPath(path) { 34 | if (arguments.length < 1) 35 | throw new TypeError( 36 | "Failed to execute 'addPath' on 'Path2D': 1 argument required, but only 0 present." 37 | ); 38 | if (!(path instanceof Path2D)) 39 | throw new TypeError( 40 | "Failed to execute 'addPath' on 'Path2D': parameter 1 is not of type 'Path2D'." 41 | ); 42 | for (let i = 0; i < path._path.length; i++) this._path.push(path._path[i]); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.shadowColor.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('shadowColor', () => { 12 | it("should parse a css color string 'blue'", () => { 13 | ctx.shadowColor = 'blue'; 14 | expect(ctx.shadowColor).toBe('#0000ff'); 15 | }); 16 | 17 | it('should not parse invalid colors', () => { 18 | ctx.shadowColor = 'invalid!'; 19 | expect(ctx.shadowColor).toBe('rgba(0, 0, 0, 0)'); 20 | }); 21 | 22 | it('should parse css colors with alpha values', () => { 23 | ctx.shadowColor = 'rgba(255, 255, 255, 0.4)'; 24 | expect(ctx.shadowColor).toBe('rgba(255, 255, 255, 0.4)'); 25 | }); 26 | 27 | it('should save and restore shadowColor values', () => { 28 | ctx.shadowColor = 'green'; 29 | ctx.save(); 30 | ctx.shadowColor = 'red'; 31 | expect(ctx.shadowColor).toBe('#ff0000'); 32 | ctx.restore(); 33 | expect(ctx.shadowColor).toBe('#008000'); 34 | }); 35 | 36 | it('should ignore invalid shadowColor values', () => { 37 | ctx.shadowColor = null; 38 | expect(ctx.shadowColor).toBe('rgba(0, 0, 0, 0)'); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.shadowBlur.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('shadowBlur', () => { 12 | it('should have a default value', () => { 13 | expect(ctx.shadowBlur).toBe(0); 14 | }); 15 | 16 | it('should ignore non finite values', () => { 17 | [Infinity, -Infinity, null, void 0, NaN].forEach((e) => { 18 | ctx.shadowBlur = e; 19 | expect(ctx.shadowBlur).toBe(0); 20 | }); 21 | }); 22 | 23 | it('should ignore out of range values', () => { 24 | [-1, -10, -300].forEach((e) => { 25 | ctx.shadowBlur = e; 26 | expect(ctx.shadowBlur).toBe(0); 27 | }); 28 | }); 29 | 30 | it('should not ignore values that are within range', () => { 31 | [1, 10, '30', '10.2'].forEach((e) => { 32 | ctx.shadowBlur = e; 33 | expect(ctx.shadowBlur).toBe(Number(e)); 34 | }); 35 | }); 36 | 37 | it('should save and restore values', () => { 38 | ctx.shadowBlur = 2; 39 | ctx.save(); 40 | ctx.shadowBlur = 10; 41 | expect(ctx.shadowBlur).toBe(10); 42 | ctx.restore(); 43 | expect(ctx.shadowBlur).toBe(2); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.miterLimit.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('miterLimit', () => { 12 | it('should have a default value', () => { 13 | expect(ctx.miterLimit).toBe(10); 14 | }); 15 | 16 | it('should ignore non finite values', () => { 17 | [Infinity, -Infinity, null, void 0, NaN].forEach((e) => { 18 | ctx.miterLimit = e; 19 | expect(ctx.miterLimit).toBe(10); 20 | }); 21 | }); 22 | 23 | it('should ignore out of range values', () => { 24 | [-1, -10, -300, 0].forEach((e) => { 25 | ctx.miterLimit = e; 26 | expect(ctx.miterLimit).toBe(10); 27 | }); 28 | }); 29 | 30 | it('should not ignore values that are within range', () => { 31 | [1, 10, '30', '10.2'].forEach((e) => { 32 | ctx.miterLimit = e; 33 | expect(ctx.miterLimit).toBe(Number(e)); 34 | }); 35 | }); 36 | 37 | it('should save and restore values', () => { 38 | ctx.miterLimit = 2; 39 | ctx.save(); 40 | ctx.miterLimit = 10; 41 | expect(ctx.miterLimit).toBe(10); 42 | ctx.restore(); 43 | expect(ctx.miterLimit).toBe(2); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.arc.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('arc', () => { 12 | it('should be a function', () => { 13 | expect(typeof ctx.arc === 'function').toBeTruthy(); 14 | }); 15 | 16 | it('should be callable', () => { 17 | ctx.arc(1, 2, 3, 4, 5); 18 | expect(ctx.arc).toBeCalled(); 19 | }); 20 | 21 | it("shouldn't accept parameters less than 7", () => { 22 | expect(() => ctx.arc()).toThrow(TypeError); 23 | expect(() => ctx.arc(1)).toThrow(TypeError); 24 | expect(() => ctx.arc(1, 2)).toThrow(TypeError); 25 | expect(() => ctx.arc(1, 2, 3)).toThrow(TypeError); 26 | expect(() => ctx.arc(1, 2, 3, 4)).toThrow(TypeError); 27 | }); 28 | 29 | it('should throw when radius is negative', () => { 30 | expect(() => ctx.arc(1, 2, -1, 4, 5)).toThrow(DOMException); 31 | }); 32 | 33 | it('should not throw if any value is `NaN`', () => { 34 | [ 35 | [NaN, 2, 3, 4, 5], 36 | [1, NaN, 3, 4, 5], 37 | [1, 2, NaN, 4, 5], 38 | [1, 2, 3, NaN, 5], 39 | [1, 2, 3, 4, NaN], 40 | ].forEach((e) => { 41 | expect(() => ctx.arc(...e)).not.toThrow(); 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.putImageData.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | const data = new ImageData(400, 300); 5 | 6 | beforeEach(() => { 7 | canvas = document.createElement('canvas'); 8 | ctx = canvas.getContext('2d'); 9 | canvas.width = 400; 10 | canvas.height = 300; 11 | }); 12 | 13 | describe('putImageData', () => { 14 | it('should be a function', () => { 15 | expect(typeof ctx.putImageData).toBe('function'); 16 | }); 17 | 18 | it('should be callable', () => { 19 | ctx.putImageData(data, 0, 0); 20 | expect(ctx.putImageData).toBeCalled(); 21 | }); 22 | 23 | it('should throw when arguments length is less than 3', () => { 24 | expect(() => ctx.putImageData()).toThrow(TypeError); 25 | expect(() => ctx.putImageData(data)).toThrow(TypeError); 26 | expect(() => ctx.putImageData(data, 1)).toThrow(TypeError); 27 | }); 28 | 29 | it('should throw when arguments length > 3 and less than 7', () => { 30 | expect(() => ctx.putImageData(data, 1, 2, 3)).toThrow(TypeError); 31 | expect(() => ctx.putImageData(data, 1, 2, 3, 4)).toThrow(TypeError); 32 | expect(() => ctx.putImageData(data, 1, 2, 3, 4, 5)).toThrow(TypeError); 33 | }); 34 | 35 | it('should throw when first argument is not of type ImageData', () => { 36 | expect(() => ctx.putImageData(null, 1, 2)).toThrow(TypeError); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasPattern.js: -------------------------------------------------------------------------------- 1 | let ctx; 2 | let canvas; 3 | const img = new Image(); 4 | img.src = 'https://placekitten.com/400/300'; 5 | 6 | beforeEach(() => { 7 | canvas = document.createElement('canvas'); 8 | canvas.width = 400; 9 | canvas.height = 300; 10 | ctx = canvas.getContext('2d'); 11 | }); 12 | 13 | describe('CanvasPattern', () => { 14 | test('CanvasPattern', () => { 15 | const ptrn = ctx.createPattern(img, 'no-repeat'); 16 | expect(ptrn).toBeDefined(); 17 | expect(ptrn).toBeInstanceOf(CanvasPattern); 18 | }); 19 | 20 | it('should have a setTransform function', () => { 21 | const ptrn = ctx.createPattern(img, 'no-repeat'); 22 | expect(typeof ptrn.setTransform).toBe('function'); 23 | }); 24 | 25 | it('should have callable setTransform', () => { 26 | const ptrn = ctx.createPattern(img, 'no-repeat'); 27 | ptrn.setTransform(ctx.getTransform()); 28 | expect(ptrn.setTransform).toBeCalled(); 29 | }); 30 | 31 | it('should throw if arguments.length > 0 and transform not instanceof Object', () => { 32 | const ptrn = ctx.createPattern(img, 'no-repeat'); 33 | expect(() => ptrn.setTransform(1)).toThrow(TypeError); 34 | }); 35 | 36 | test('CanvasPattern different instance', () => { 37 | const ptrn1 = ctx.createPattern(img, 'no-repeat'); 38 | const ptrn2 = ctx.createPattern(img, 'no-repeat'); 39 | expect(ptrn1.setTransform).not.toBe(ptrn2.setTransform); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.arcTo.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('arcTo', () => { 12 | it('should be a function', () => { 13 | expect(typeof ctx.arcTo).toBe('function'); 14 | }); 15 | 16 | it('should be callable', () => { 17 | ctx.arcTo(1, 2, 3, 4, 5); 18 | expect(ctx.arcTo).toBeCalled(); 19 | }); 20 | 21 | it("shouldn't accept parameters less than 5", () => { 22 | expect(() => ctx.arcTo(1, 2, 3)).toThrow(DOMException); 23 | }); 24 | 25 | it('should throw when radius is negative', () => { 26 | expect(() => ctx.arcTo(1, 2, -1, 3, -1)).toThrow(TypeError); 27 | }); 28 | 29 | it('should accept 5 parameters regardless of type', () => { 30 | [ 31 | [1, 2, 3, 4, 5], 32 | [null, void 0, '', NaN, Infinity], 33 | [-100, -100, 100, 0, 0], 34 | ].forEach((e) => { 35 | expect(() => ctx.arcTo(...e)).not.toThrow(); 36 | }); 37 | }); 38 | 39 | it('should not throw for negative radius values if either control point is not a valid point', () => { 40 | [ 41 | [NaN, 1, 2, 3, -1], 42 | [1, NaN, 2, 3, -1], 43 | [1, 2, NaN, 3, -1], 44 | [1, 2, 3, NaN, -1], 45 | ].forEach((e) => { 46 | expect(() => ctx.arcTo(...e)).not.toThrow(); 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.lineWidth.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('lineWidth', () => { 12 | it('should have a default lineWidth value', () => { 13 | expect(ctx.lineWidth).toBe(1); 14 | }); 15 | 16 | it('should ignore non finite lineWidth values', () => { 17 | [Infinity, -Infinity, null, void 0, NaN].forEach((e) => { 18 | ctx.lineWidth = e; 19 | expect(ctx.lineWidth).toBe(1); 20 | }); 21 | }); 22 | 23 | it('should ignore out of range lineWidth values', () => { 24 | [-1, -10, -300, 0].forEach((e) => { 25 | ctx.lineWidth = e; 26 | expect(ctx.lineWidth).toBe(1); 27 | }); 28 | }); 29 | 30 | it('should not ignore lineWidth values that are within range', () => { 31 | [1, 10, '30', '10.2'].forEach((e) => { 32 | ctx.lineWidth = e; 33 | expect(ctx.lineWidth).toBe(Number(e)); 34 | }); 35 | }); 36 | 37 | it('should save and restore lineWidth values', () => { 38 | ctx.lineWidth = 2; 39 | ctx.save(); 40 | ctx.lineWidth = 10; 41 | expect(ctx.lineWidth).toBe(10); 42 | ctx.save(); 43 | expect(ctx.lineWidth).toBe(10); 44 | ctx.restore(); 45 | expect(ctx.lineWidth).toBe(10); 46 | ctx.restore(); 47 | expect(ctx.lineWidth).toBe(2); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.drawFocusIfNeeded.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | const elem = document.createElement('div'); 4 | const path = new Path2D(); 5 | 6 | beforeEach(() => { 7 | canvas = document.createElement('canvas'); 8 | ctx = canvas.getContext('2d'); 9 | canvas.width = 400; 10 | canvas.height = 300; 11 | }); 12 | 13 | describe('drawFocusIfNeeded', () => { 14 | it('should be a function', () => { 15 | expect(typeof ctx.drawFocusIfNeeded).toBe('function'); 16 | }); 17 | 18 | it('should be callable', () => { 19 | ctx.drawFocusIfNeeded(path, elem); 20 | expect(ctx.drawFocusIfNeeded).toBeCalled(); 21 | }); 22 | 23 | it('should throw if first argument is not provided', () => { 24 | expect(() => ctx.drawFocusIfNeeded()).toThrow(TypeError); 25 | }); 26 | 27 | it('should throw if first argument is not Element', () => { 28 | [1, NaN, Infinity, 'test', {}, []].forEach((e) => { 29 | expect(() => ctx.drawFocusIfNeeded(e)).toThrow(TypeError); 30 | }); 31 | }); 32 | 33 | it('should throw if two parameters are provided and the first parameter is not a path', () => { 34 | [1, NaN, Infinity, 'test', {}, []].forEach((e) => { 35 | expect(() => ctx.drawFocusIfNeeded(e, elem)).toThrow(TypeError); 36 | }); 37 | }); 38 | 39 | it('should not throw if two arguments are provided and they match the signature', () => { 40 | expect(() => ctx.drawFocusIfNeeded(path, elem)).not.toThrow(); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.isPointInPath.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | const p = new Path2D(); 4 | 5 | beforeEach(() => { 6 | canvas = document.createElement('canvas'); 7 | ctx = canvas.getContext('2d'); 8 | canvas.width = 400; 9 | canvas.height = 300; 10 | }); 11 | 12 | describe('isPointInPath', () => { 13 | it('should be a function', () => { 14 | expect(typeof ctx.isPointInPath).toBe('function'); 15 | }); 16 | 17 | it('should be callable', () => { 18 | ctx.isPointInPath(1, 2); 19 | expect(ctx.isPointInPath).toBeCalled(); 20 | }); 21 | 22 | it('should throw if less than 2 arguments are provided', () => { 23 | expect(() => ctx.isPointInPath(1)).toThrow(TypeError); 24 | expect(() => ctx.isPointInPath()).toThrow(TypeError); 25 | }); 26 | 27 | it('should always return false', () => { 28 | expect(ctx.isPointInPath(1, 2)).toBeFalsy(); 29 | expect(ctx.isPointInPath(p, 1, 2)).toBeFalsy(); 30 | }); 31 | 32 | it('should throw if provided fillRule is not valid', () => { 33 | expect(() => ctx.isPointInPath(1, 2, 'wrong!')).toThrow(TypeError); 34 | expect(() => ctx.isPointInPath(p, 1, 2, 'wrong!')).toThrow(TypeError); 35 | }); 36 | 37 | it('should accept valid fillRules', () => { 38 | expect(() => ctx.isPointInPath(1, 2, 'evenodd')).not.toThrow(); 39 | expect(() => ctx.isPointInPath(1, 2, 'nonzero')).not.toThrow(); 40 | expect(() => ctx.isPointInPath(p, 1, 2, 'evenodd')).not.toThrow(); 41 | expect(() => ctx.isPointInPath(p, 1, 2, 'nonzero')).not.toThrow(); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.createImageData.js: -------------------------------------------------------------------------------- 1 | let ctx; 2 | let canvas; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | }); 8 | 9 | describe('createImageData', () => { 10 | it('should be a function', () => { 11 | expect(typeof ctx.createImageData).toBe('function'); 12 | }); 13 | 14 | it('should be callable', () => { 15 | ctx.createImageData(1, 2); 16 | expect(ctx.createImageData).toBeCalled(); 17 | }); 18 | 19 | it('should return an ImageData', () => { 20 | const result = ctx.createImageData(1, 2); 21 | expect(result).toBeInstanceOf(ImageData); 22 | }); 23 | 24 | it('should throw if arguments length is 0', () => { 25 | expect(() => ctx.createImageData()).toThrow(TypeError); 26 | }); 27 | 28 | it('should throw if arguments.length === 1 and first parameter is not image data', () => { 29 | expect(() => ctx.createImageData(1)).toThrow(TypeError); 30 | }); 31 | 32 | it('should return a new ImageData if first parameter is ImageData', () => { 33 | const input = new ImageData(100, 100); 34 | const result = ctx.createImageData(input); 35 | expect(input).not.toBe(result); 36 | expect(result).toBeInstanceOf(ImageData); 37 | }); 38 | 39 | it('should throw if two parameters are provided and any of them are not finite or 0', () => { 40 | [ 41 | [0, 0], 42 | [NaN, 1], 43 | [1, NaN], 44 | ['test', null], 45 | ].forEach((value) => { 46 | expect(() => ctx.createImageData(...value)).toThrow(TypeError); 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.currentTransform.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('currentTransform', () => { 12 | it('should return a DOMMatrix when accessing the currentTransform property', () => { 13 | expect(ctx.currentTransform).toBeInstanceOf(DOMMatrix); 14 | }); 15 | 16 | it("should ignore setting currentTransform if it's not a valid DOMMatrix", () => { 17 | ctx.currentTransform = null; 18 | expect(ctx._transformStack[0][0]).toBe(1); 19 | expect(ctx._transformStack[0][1]).toBe(0); 20 | expect(ctx._transformStack[0][2]).toBe(0); 21 | expect(ctx._transformStack[0][3]).toBe(1); 22 | expect(ctx._transformStack[0][4]).toBe(0); 23 | expect(ctx._transformStack[0][5]).toBe(0); 24 | }); 25 | 26 | it('should return a DOMMatrix when calling getTransform()', () => { 27 | expect(ctx.getTransform()).toBeInstanceOf(DOMMatrix); 28 | }); 29 | 30 | it('should set the current transform of the context when setting the currentTransform property', () => { 31 | const matrix = new DOMMatrix([1, 2, 3, 4, 5, 6]); 32 | ctx.currentTransform = matrix; 33 | expect(ctx._transformStack[0][0]).toBe(1); 34 | expect(ctx._transformStack[0][1]).toBe(2); 35 | expect(ctx._transformStack[0][2]).toBe(3); 36 | expect(ctx._transformStack[0][3]).toBe(4); 37 | expect(ctx._transformStack[0][4]).toBe(5); 38 | expect(ctx._transformStack[0][5]).toBe(6); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.createLinearGradient.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('createLinearGradient', () => { 12 | it('should be a function', () => { 13 | expect(typeof ctx.createLinearGradient).toBe('function'); 14 | }); 15 | 16 | it('should be callable', () => { 17 | ctx.createLinearGradient(1, 2, 3, 4, 5, 6); 18 | expect(ctx.createLinearGradient).toBeCalled(); 19 | }); 20 | 21 | it('should not create a linear gradient when the argument length is < 4', () => { 22 | expect(() => ctx.createLinearGradient()).toThrow(TypeError); 23 | expect(() => ctx.createLinearGradient(0)).toThrow(TypeError); 24 | expect(() => ctx.createLinearGradient(0, 1)).toThrow(TypeError); 25 | expect(() => ctx.createLinearGradient(0, 1, 2)).toThrow(TypeError); 26 | }); 27 | 28 | it('should not create a linear gradient when any argument is not finite', () => { 29 | expect(() => ctx.createLinearGradient(Infinity, 1, 2, 3)).toThrow( 30 | TypeError 31 | ); 32 | expect(() => ctx.createLinearGradient(0, Infinity, 2, 3)).toThrow( 33 | TypeError 34 | ); 35 | expect(() => ctx.createLinearGradient(0, 1, Infinity, 3)).toThrow( 36 | TypeError 37 | ); 38 | expect(() => ctx.createLinearGradient(0, 1, 2, Infinity)).toThrow( 39 | TypeError 40 | ); 41 | }); 42 | 43 | it('should create a linear gradient with string values', () => { 44 | const grd = ctx.createLinearGradient('1', '2', '3', '4'); 45 | expect(grd).toBeInstanceOf(CanvasGradient); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.ellipse.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('ellipse', () => { 12 | it('should be a function', () => { 13 | expect(typeof ctx.ellipse === 'function').toBeTruthy(); 14 | }); 15 | 16 | it('should be callable', () => { 17 | ctx.ellipse(1, 2, 3, 4, 5, 6, 7); 18 | expect(ctx.ellipse).toBeCalled(); 19 | }); 20 | 21 | it("shouldn't accept parameters less than 7", () => { 22 | expect(() => ctx.ellipse()).toThrow(TypeError); 23 | expect(() => ctx.ellipse(1)).toThrow(TypeError); 24 | expect(() => ctx.ellipse(1, 2)).toThrow(TypeError); 25 | expect(() => ctx.ellipse(1, 2, 3)).toThrow(TypeError); 26 | expect(() => ctx.ellipse(1, 2, 3, 4)).toThrow(TypeError); 27 | expect(() => ctx.ellipse(1, 2, 3, 4, 5)).toThrow(TypeError); 28 | expect(() => ctx.ellipse(1, 2, 3, 4, 5, 6)).toThrow(TypeError); 29 | }); 30 | 31 | it('should throw when radius is negative', () => { 32 | expect(() => ctx.ellipse(1, 2, -1, 4, 5, 6, 7)).toThrow(DOMException); 33 | expect(() => ctx.ellipse(1, 2, 3, -1, 5, 6, 7)).toThrow(DOMException); 34 | }); 35 | 36 | it('should not throw if any value is `NaN`', () => { 37 | [ 38 | [NaN, 2, 3, 4, 5, 6, 7], 39 | [1, NaN, 3, 4, 5, 6, 7], 40 | [1, 2, NaN, 4, 5, 6, 7], 41 | [1, 2, 3, NaN, 5, 6, 7], 42 | [1, 2, 3, 4, NaN, 6, 7], 43 | [1, 2, 3, 4, 5, NaN, 7], 44 | [1, 2, 3, 4, 5, 6, NaN], 45 | ].forEach((e) => { 46 | expect(() => ctx.ellipse(...e)).not.toThrow(); 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.fillStyle.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('fillStyle', () => { 12 | it("should parse a css color string 'blue'", () => { 13 | ctx.fillStyle = 'blue'; 14 | expect(ctx.fillStyle).toBe('#0000ff'); 15 | }); 16 | 17 | it('should not parse invalid colors', () => { 18 | ctx.fillStyle = 'invalid!'; 19 | expect(ctx.fillStyle).toBe('#000000'); 20 | }); 21 | 22 | it('should parse css colors with alpha values', () => { 23 | ctx.fillStyle = 'rgba(255, 255, 255, 0.4)'; 24 | expect(ctx.fillStyle).toBe('rgba(255, 255, 255, 0.4)'); 25 | }); 26 | 27 | it('should save and restore fillStyle values', () => { 28 | ctx.fillStyle = 'green'; 29 | ctx.save(); 30 | ctx.fillStyle = 'red'; 31 | expect(ctx.fillStyle).toBe('#ff0000'); 32 | ctx.restore(); 33 | expect(ctx.fillStyle).toBe('#008000'); 34 | }); 35 | 36 | it('should accept CanvasPatterns as valid fillStyle values', () => { 37 | const image = new Image(); 38 | image.src = 'test.png'; 39 | const pattern = ctx.createPattern(image, 'no-repeat'); 40 | ctx.fillStyle = pattern; 41 | expect(ctx.fillStyle).toBe(pattern); 42 | }); 43 | 44 | it('should accept CanvasGradients as valid fillStyle values', () => { 45 | const grd = ctx.createRadialGradient(1, 2, 3, 4, 5, 6); 46 | ctx.fillStyle = grd; 47 | expect(ctx.fillStyle).toBe(grd); 48 | }); 49 | 50 | it('should ignore invalid fillStyle values', () => { 51 | ctx.fillStyle = null; 52 | expect(ctx.fillStyle).toBe('#000000'); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.transform.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | ctx.setTransform(1, 2, 3, 4, 5, 6); 10 | }); 11 | 12 | describe('transform', () => { 13 | it('should be a function', () => { 14 | expect(typeof ctx.transform).toBe('function'); 15 | }); 16 | 17 | it('should be callable', () => { 18 | ctx.transform(1, 2, 3, 4, 5, 6); 19 | expect(ctx.transform).toBeCalled(); 20 | }); 21 | 22 | it('should transform the current transform', () => { 23 | ctx.transform(1, 2, 3, 4, 5, 6); 24 | expect(ctx.currentTransform).toEqual( 25 | new DOMMatrix([7, 10, 15, 22, 28, 40]) 26 | ); 27 | }); 28 | 29 | it('should throw if argument count is less than 6', () => { 30 | expect(() => ctx.transform()).toThrow(TypeError); 31 | expect(() => ctx.transform(1)).toThrow(TypeError); 32 | expect(() => ctx.transform(1, 2)).toThrow(TypeError); 33 | expect(() => ctx.transform(1, 2, 3)).toThrow(TypeError); 34 | expect(() => ctx.transform(1, 2, 3, 4)).toThrow(TypeError); 35 | expect(() => ctx.transform(1, 2, 3, 4, 5)).toThrow(TypeError); 36 | }); 37 | 38 | it('should not transform the transform if any of the values cannot be coerced into finite numbers', () => { 39 | [ 40 | [NaN, 2, 3, 4, 5, 6], 41 | [1, NaN, 2, 3, 4, 5], 42 | [1, 2, NaN, 3, 4, 5], 43 | [1, 2, 3, NaN, 4, 5], 44 | [1, 2, 3, 4, NaN, 6], 45 | [1, 2, 3, 4, 5, NaN], 46 | ].forEach((e) => { 47 | ctx.transform(...e); 48 | expect(ctx.currentTransform).toEqual(new DOMMatrix([1, 2, 3, 4, 5, 6])); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.strokeStyle.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('strokeStyle', () => { 12 | it("should parse a css color string 'blue'", () => { 13 | ctx.strokeStyle = 'blue'; 14 | expect(ctx.strokeStyle).toBe('#0000ff'); 15 | }); 16 | 17 | it('should not parse invalid colors', () => { 18 | ctx.strokeStyle = 'invalid!'; 19 | expect(ctx.strokeStyle).toBe('#000000'); 20 | }); 21 | 22 | it('should parse css colors with alpha values', () => { 23 | ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)'; 24 | expect(ctx.strokeStyle).toBe('rgba(255, 255, 255, 0.4)'); 25 | }); 26 | 27 | it('should save and restore strokeStyle values', () => { 28 | ctx.strokeStyle = 'green'; 29 | ctx.save(); 30 | ctx.strokeStyle = 'red'; 31 | expect(ctx.strokeStyle).toBe('#ff0000'); 32 | ctx.restore(); 33 | expect(ctx.strokeStyle).toBe('#008000'); 34 | }); 35 | 36 | it('should accept CanvasPatterns as valid strokeStyle values', () => { 37 | const image = new Image(); 38 | image.src = 'test.png'; 39 | const pattern = ctx.createPattern(image, 'no-repeat'); 40 | ctx.strokeStyle = pattern; 41 | expect(ctx.strokeStyle).toBe(pattern); 42 | }); 43 | 44 | it('should accept CanvasGradients as valid strokeStyle values', () => { 45 | const grd = ctx.createRadialGradient(1, 2, 3, 4, 5, 6); 46 | ctx.strokeStyle = grd; 47 | expect(ctx.strokeStyle).toBe(grd); 48 | }); 49 | 50 | it('should ignore invalid strokeStyle values', () => { 51 | ctx.strokeStyle = null; 52 | expect(ctx.strokeStyle).toBe('#000000'); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.globalCompositeOperation.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('globalCompositeOperation', () => { 12 | it("should change the global composite operation when it's valid", () => { 13 | const validOperations = [ 14 | 'source-over', 15 | 'source-in', 16 | 'source-out', 17 | 'source-atop', 18 | 'destination-over', 19 | 'destination-in', 20 | 'destination-out', 21 | 'destination-atop', 22 | 'lighter', 23 | 'copy', 24 | 'xor', 25 | 'multiply', 26 | 'screen', 27 | 'overlay', 28 | 'darken', 29 | 'lighten', 30 | 'color-dodge', 31 | 'color-burn', 32 | 'hard-light', 33 | 'soft-light', 34 | 'difference', 35 | 'exclusion', 36 | 'hue', 37 | 'saturation', 38 | 'color', 39 | 'luminosity', 40 | ]; 41 | validOperations.forEach((e) => { 42 | ctx.globalCompositeOperation = e; 43 | expect(ctx.globalCompositeOperation).toBe(e); 44 | }); 45 | }); 46 | 47 | it('should ignore non valid values', () => { 48 | [null, -1, void 0, Infinity, NaN, 'blah', ''].forEach((e) => { 49 | ctx.globalCompositeOperation = e; 50 | expect(ctx.globalCompositeOperation).toBe('source-over'); 51 | }); 52 | }); 53 | 54 | it('should save and restore composite values', () => { 55 | ctx.save(); 56 | ctx.globalCompositeOperation = 'source-in'; 57 | expect(ctx.globalCompositeOperation).toBe('source-in'); 58 | ctx.restore(); 59 | expect(ctx.globalCompositeOperation).toBe('source-over'); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.__getClippingPath.js: -------------------------------------------------------------------------------- 1 | let ctx; 2 | beforeEach(() => { 3 | // get a new context each test 4 | ctx = document.createElement('canvas').getContext('2d'); 5 | }); 6 | 7 | afterEach(() => { 8 | const drawCalls = ctx.__getClippingRegion(); 9 | expect(drawCalls).toMatchSnapshot(); 10 | }); 11 | 12 | describe('__getClippingRegion', () => { 13 | it('should be empty when there are no path elements', () => { 14 | ctx.clip(); 15 | }); 16 | 17 | it('should store the clipping region', () => { 18 | ctx.rect(1, 2, 3, 4); 19 | ctx.arc(1, 2, 3, 4, 5); 20 | ctx.clip(); 21 | }); 22 | 23 | it("shouldn't store the whole clipping region twice when clip is called twice", () => { 24 | ctx.rect(1, 2, 3, 4); 25 | ctx.arc(1, 2, 3, 4, 5); 26 | ctx.clip(); 27 | ctx.rect(1, 2, 3, 4); 28 | ctx.arc(1, 2, 3, 4, 5); 29 | ctx.clip(); 30 | }); 31 | 32 | it('should save the clipping region correctly when saved', () => { 33 | ctx.rect(1, 2, 3, 4); 34 | ctx.arc(1, 2, 3, 4, 5); 35 | ctx.clip(); 36 | const region = ctx.__getClippingRegion(); 37 | ctx.save(); 38 | expect(region).toStrictEqual(ctx.__getClippingRegion()); 39 | }); 40 | 41 | it('should save the clipping region correctly when saved', () => { 42 | ctx.rect(1, 2, 3, 4); 43 | ctx.arc(1, 2, 3, 4, 5); 44 | ctx.clip(); 45 | const region = ctx.__getClippingRegion(); 46 | ctx.save(); 47 | ctx.rect(1, 2, 3, 4); 48 | ctx.arc(1, 2, 3, 4, 5); 49 | ctx.clip(); 50 | expect(region).not.toStrictEqual(ctx.__getClippingRegion()); 51 | ctx.restore(); 52 | expect(region).toStrictEqual(ctx.__getClippingRegion()); 53 | }); 54 | 55 | it('should delete current clipping region when restored', () => { 56 | ctx.rect(1, 2, 3, 4); 57 | ctx.clip(); 58 | ctx.save(); 59 | ctx.rect(1, 2, 3, 4); 60 | ctx.arc(1, 2, 3, 4, 5); 61 | ctx.clip(); 62 | ctx.restore(); 63 | ctx.save(); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.createRadialGradient.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('createRadialGradient', () => { 12 | it('should be a function', () => { 13 | expect(typeof ctx.createRadialGradient).toBe('function'); 14 | }); 15 | 16 | it('should be callable', () => { 17 | ctx.createRadialGradient(1, 2, 3, 4, 5, 6); 18 | expect(ctx.createRadialGradient).toBeCalled(); 19 | }); 20 | 21 | it('should not create a radial gradient when the argument length is < 6', () => { 22 | expect(() => ctx.createRadialGradient(0, 1, 2, 3, 4)).toThrow(TypeError); 23 | }); 24 | 25 | it('should not create a radial gradient when any argument is not finite', () => { 26 | expect(() => ctx.createRadialGradient(Infinity, 1, 2, 3, 4, 5)).toThrow( 27 | TypeError 28 | ); 29 | expect(() => ctx.createRadialGradient(0, Infinity, 2, 3, 4, 5)).toThrow( 30 | TypeError 31 | ); 32 | expect(() => ctx.createRadialGradient(0, 1, Infinity, 3, 4, 5)).toThrow( 33 | TypeError 34 | ); 35 | expect(() => ctx.createRadialGradient(0, 1, 2, Infinity, 4, 5)).toThrow( 36 | TypeError 37 | ); 38 | expect(() => ctx.createRadialGradient(0, 1, 2, 3, Infinity, 5)).toThrow( 39 | TypeError 40 | ); 41 | expect(() => ctx.createRadialGradient(0, 1, 2, 3, 4, Infinity)).toThrow( 42 | TypeError 43 | ); 44 | }); 45 | 46 | it('should not create a radial gradient if any of the radius values are < 0', () => { 47 | expect(() => ctx.createRadialGradient(0, 0, -1, 0, 0, 0)).toThrow( 48 | DOMException 49 | ); 50 | expect(() => ctx.createRadialGradient(0, 0, 0, 0, 0, -1)).toThrow( 51 | DOMException 52 | ); 53 | }); 54 | 55 | it('should create a radial gradient with string values', () => { 56 | const result = ctx.createRadialGradient('1', '2', '3', '4', '5', '6'); 57 | expect(result).toBeInstanceOf(CanvasGradient); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.drawImage.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | const img = new Image(); 4 | img.src = 'https://placekitten.com/400/300'; 5 | beforeEach(() => { 6 | canvas = document.createElement('canvas'); 7 | ctx = canvas.getContext('2d'); 8 | canvas.width = 400; 9 | canvas.height = 300; 10 | }); 11 | 12 | describe('drawImage', () => { 13 | it('should be a function', () => { 14 | expect(typeof ctx.drawImage).toBe('function'); 15 | }); 16 | 17 | it('should be callable', () => { 18 | ctx.drawImage(img, 1, 2); 19 | expect(ctx.drawImage).toBeCalled(); 20 | }); 21 | 22 | it('should draw when image is Video', () => { 23 | const video = document.createElement('video'); 24 | expect(() => ctx.drawImage(video, 1, 2)).not.toThrow(); 25 | }); 26 | 27 | it('should draw when image is Canvas', () => { 28 | const canvas = document.createElement('canvas'); 29 | expect(() => ctx.drawImage(canvas, 1, 2)).not.toThrow(); 30 | }); 31 | 32 | it('should draw when image is ImageBitmap', () => { 33 | expect(() => ctx.drawImage(new ImageBitmap(100, 100), 1, 2)).not.toThrow(); 34 | }); 35 | 36 | it('should not draw if the image bitmap is closed', () => { 37 | const bmp = new ImageBitmap(100, 100); 38 | bmp.close(); 39 | expect(() => ctx.drawImage(bmp, 1, 2)).toThrow(DOMException); 40 | }); 41 | 42 | it('should accept 3, 5, and 9 parameters', () => { 43 | expect(() => ctx.drawImage(img, 1, 2)).not.toThrow(); 44 | expect(() => ctx.drawImage(img, 1, 2, 3, 4)).not.toThrow(); 45 | expect(() => ctx.drawImage(img, 1, 2, 3, 4, 5, 6, 7, 8)).not.toThrow(); 46 | }); 47 | 48 | it('should not accept, 1, 2, 4, 6, 7, 8 parameters', () => { 49 | [ 50 | [img], 51 | [img, 1], 52 | [img, 1, 2, 3], 53 | [img, 1, 2, 3, 4, 5], 54 | [img, 1, 2, 3, 4, 5, 6], 55 | [img, 1, 2, 3, 4, 5, 6, 7], 56 | ].forEach((e) => { 57 | expect(() => ctx.drawImage(...e)).toThrow(TypeError); 58 | }); 59 | }); 60 | 61 | it('should not accept nulls or invalid image types', () => { 62 | [null, 1, NaN, Infinity, 'testing'].forEach((e) => { 63 | expect(() => ctx.drawImage(e, 1, 2)).toThrow(TypeError); 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /__tests__/classes/Path2D.js: -------------------------------------------------------------------------------- 1 | import mockWindow from '../../src/window'; 2 | 3 | let borrowedFromCanvas = [ 4 | 'closePath', 5 | 'moveTo', 6 | 'lineTo', 7 | 'bezierCurveTo', 8 | 'quadraticCurveTo', 9 | 'arc', 10 | 'arcTo', 11 | 'ellipse', 12 | 'rect', 13 | ]; 14 | 15 | describe('Path2D', () => { 16 | test('Path2D', () => { 17 | const path = new Path2D(); 18 | expect(path).toBeInstanceOf(Path2D); 19 | }); 20 | 21 | it('should have addPath function', () => { 22 | const p = new Path2D(); 23 | expect(typeof p.addPath).toBe('function'); 24 | }); 25 | 26 | it('should have a callable addPath function', () => { 27 | const p = new Path2D(); 28 | p.addPath(new Path2D()); 29 | expect(p.addPath).toBeCalled(); 30 | }); 31 | 32 | it('should borrow some path functions from CanvasRenderingContext2D', () => { 33 | const p = new Path2D(); 34 | borrowedFromCanvas.forEach((func) => { 35 | expect(typeof p[func]).toBe('function'); 36 | }); 37 | }); 38 | 39 | it('should throw if addPath is called with no parameters', () => { 40 | expect(() => new Path2D().addPath()).toThrow(TypeError); 41 | }); 42 | 43 | it('should throw if first argument is not Path2D', () => { 44 | const p = new Path2D(); 45 | [null, 1, void 0, NaN, Infinity, {}, []].forEach((item) => { 46 | expect(() => p.addPath(item)).toThrow(TypeError); 47 | }); 48 | }); 49 | 50 | test('Path2D different instance', () => { 51 | const path1 = new Path2D(); 52 | const path2 = new Path2D(); 53 | expect(path1.addPath).not.toBe(path2.addPath); 54 | }); 55 | 56 | test('Path2D not override', () => { 57 | const saved = window.Path2D; 58 | mockWindow(window); 59 | expect(saved === window.Path2D).toBe(true); 60 | }); 61 | 62 | test('Path2D addPath calls _path.push', () => { 63 | const path1 = new Path2D(); 64 | path1.moveTo(10, 10); 65 | path1.lineTo(20, 20); 66 | const path2 = new Path2D(); 67 | path2.moveTo(30, 30); 68 | path2.lineTo(40, 40); 69 | expect(path1._path.length).toBe(2); 70 | path1.addPath(path2); 71 | expect(path1._path.length).toBe(4); 72 | expect(path1._path[2]).toBe(path2._path[0]); 73 | expect(path1._path[3]).toBe(path2._path[1]); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasGradient.js: -------------------------------------------------------------------------------- 1 | let ctx; 2 | let grd; 3 | 4 | beforeEach(() => { 5 | ctx = document.createElement('canvas').getContext('2d'); 6 | grd = ctx.createLinearGradient(1, 2, 3, 4); 7 | }); 8 | 9 | describe('CanvasGradient', () => { 10 | test('CanvasGradient', () => { 11 | expect(grd).toBeDefined(); 12 | grd.addColorStop(1.0, 'blue'); 13 | expect(grd.addColorStop).toBeCalledWith(1.0, 'blue'); 14 | 15 | const grd2 = ctx.createLinearGradient(2, 3, 4, 5); 16 | expect(grd2.addColorStop).not.toBeCalled(); 17 | }); 18 | 19 | test('CanvasGradient different instance', () => { 20 | const grd1 = ctx.createLinearGradient(1, 2, 3, 4); 21 | const grd2 = ctx.createLinearGradient(1, 2, 3, 4); 22 | expect(grd1.addColorStop).not.toBe(grd2.addColorStop); 23 | }); 24 | 25 | [Infinity, NaN, -Infinity].forEach((value) => { 26 | test('CanvasGradient should throw if offset is ' + value, () => { 27 | expect(() => { 28 | var grd = ctx.createLinearGradient(1, 2, 3, 4); 29 | grd.addColorStop(value, 'blue'); 30 | }).toThrow(DOMException); 31 | }); 32 | }); 33 | 34 | test('should accept all valid CSS colors as a color stop', () => { 35 | const grd = ctx.createLinearGradient(0, 0, 100, 100); 36 | expect(() => { 37 | grd.addColorStop(0.1, 'blue'); 38 | grd.addColorStop(0.2, '#f008'); 39 | grd.addColorStop(0.2, '#f0f0f0'); 40 | grd.addColorStop(0.3, '#f0f0f0f0'); 41 | grd.addColorStop(0.3, 'rgb(10, 10, 10)'); 42 | grd.addColorStop(0.4, 'rgb(100%, 25%, 25%)'); 43 | grd.addColorStop(0.3, 'rgba(100, 100, 100, 0.5)'); 44 | grd.addColorStop(0.4, 'hsl(180, 100%, 50%)'); 45 | grd.addColorStop(0.5, 'hsla(180, 100%, 50%, 0.5)'); 46 | grd.addColorStop(0.0, 'transparent'); 47 | }).not.toThrow(SyntaxError); 48 | }); 49 | 50 | test('CanvasGradient should throw if color cannot be parsed', () => { 51 | const grd = ctx.createLinearGradient(1, 2, 3, 4); 52 | expect(() => { 53 | grd.addColorStop(0.5, 'invalid'); 54 | }).toThrow(SyntaxError); 55 | 56 | expect(() => { 57 | grd.addColorStop(0.5, 'rgb(50%, 0, 50%)'); 58 | }).toThrow(SyntaxError); 59 | 60 | expect(() => { 61 | grd.addColorStop(0.5, 'hsl(180, 50%, 50)'); 62 | }).toThrow(SyntaxError); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.lineDash.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | function every(items, callback) { 12 | for (let i = 0; i < items.length; i++) { 13 | if (callback(items[i])) { 14 | continue; 15 | } 16 | return false; 17 | } 18 | return true; 19 | } 20 | 21 | function map(items, callback) { 22 | const result = []; 23 | for (let i = 0; i < items.length; i++) { 24 | result.push(callback(items[i])); 25 | } 26 | return result; 27 | } 28 | 29 | const isSequence = (value) => 30 | [ 31 | Array, 32 | Int8Array, 33 | Uint8Array, 34 | Int16Array, 35 | Uint16Array, 36 | Int32Array, 37 | Uint32Array, 38 | Float32Array, 39 | Float64Array, 40 | ].reduce((left, right) => left || value instanceof right, false); 41 | 42 | describe('lineDash', () => { 43 | it('should accept valid lineDash values ignore invalid lineDash values', () => { 44 | const examples = [ 45 | [1, 2, 3, 4], 46 | [1, 2, 3], 47 | [null, 4, 2], 48 | [Infinity, -1, 4], 49 | ['1', '2', '3'], 50 | new Float64Array([1, 2, 3, 4]), 51 | 'blah', 52 | 0, 53 | -1, 54 | Infinity, 55 | null, 56 | ]; 57 | 58 | examples.forEach((e) => { 59 | ctx.setLineDash([]); // reset the linedash 60 | if (!isSequence(e)) { 61 | expect(() => ctx.setLineDash(e)).toThrow(TypeError); 62 | } else { 63 | ctx.setLineDash(e); 64 | let result = map(e, (val) => Number(val)); 65 | const containsFiniteValues = every(result, (val) => 66 | Number.isFinite(val) 67 | ); 68 | if (containsFiniteValues) { 69 | result = result.length % 2 === 1 ? result.concat(result) : result; 70 | } else { 71 | result = []; 72 | } 73 | expect(ctx.getLineDash()).toEqual(result); 74 | } 75 | }); 76 | }); 77 | 78 | it('should save and restore lineDash values', () => { 79 | ctx.save(); 80 | ctx.setLineDash([1, 2, 3]); 81 | expect(ctx.getLineDash()).toEqual([1, 2, 3, 1, 2, 3]); 82 | ctx.restore(); 83 | expect(ctx.getLineDash()).toEqual([]); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jest-webgl-canvas-mock", 3 | "version": "2.5.0", 4 | "description": "Mock both 2D and WebGL contexts in Jest.", 5 | "main": "lib/index.js", 6 | "types": "types/index.d.ts", 7 | "publishConfig": { 8 | "registry": "https://registry.npmjs.org/" 9 | }, 10 | "scripts": { 11 | "test": "jest --no-cache", 12 | "build": "babel src --out-dir lib", 13 | "prepare": "npm run build", 14 | "prettier": "prettier --write ." 15 | }, 16 | "dependencies": { 17 | "cssfontparser": "^1.2.1", 18 | "moo-color": "^1.0.2" 19 | }, 20 | "devDependencies": { 21 | "@antv/g2plot": "^2.3.11", 22 | "@babel/cli": "^7.8.4", 23 | "@babel/core": "^7.9.0", 24 | "@babel/plugin-proposal-class-properties": "^7.8.3", 25 | "@babel/preset-env": "^7.9.5", 26 | "@commitlint/cli": "^17.6.1", 27 | "@commitlint/config-angular": "^17.6.1", 28 | "babel-jest": "^25.3.0", 29 | "babel-plugin-version": "^0.2.3", 30 | "husky": "^4.2.5", 31 | "jest": "^29.5.0", 32 | "jest-environment-jsdom": "^29.5.0", 33 | "prettier": "^2.0.4" 34 | }, 35 | "commitlint": { 36 | "extends": [ 37 | "@commitlint/config-angular" 38 | ] 39 | }, 40 | "husky": { 41 | "hooks": { 42 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", 43 | "pre-commit": "npm run prettier && npm run test && npm run build" 44 | } 45 | }, 46 | "jest": { 47 | "testEnvironment": "jsdom", 48 | "collectCoverage": true, 49 | "collectCoverageFrom": [ 50 | "src/classes/**/*.js", 51 | "src/mock/**/*.js" 52 | ], 53 | "setupFiles": [ 54 | "./src/index.js", 55 | "./__mocks__/worker.js" 56 | ] 57 | }, 58 | "repository": { 59 | "type": "git", 60 | "url": "git+https://github.com/adamfsk/jest-webgl-canvas-mock.git" 61 | }, 62 | "keywords": [ 63 | "mock", 64 | "jest", 65 | "jest-mock", 66 | "echarts", 67 | "canvas", 68 | "test", 69 | "unit", 70 | "stub", 71 | "webgl", 72 | "pixi", 73 | "pixi.js", 74 | "pixijs" 75 | ], 76 | "files": [ 77 | "CHANGELOG.md", 78 | "lib/", 79 | "types/" 80 | ], 81 | "author": "adamfsk", 82 | "license": "MIT", 83 | "bugs": { 84 | "url": "https://github.com/adamfsk/jest-webgl-canvas-mock/issues" 85 | }, 86 | "homepage": "https://github.com/adamfsk/jest-webgl-canvas-mock#readme" 87 | } 88 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | The following is a set of guidelines for contributing to jest-canvas-mock. These are mostly guidelines, not rules. Use your best judgement, and feel free to proposse changes to this document in a pull request. 4 | 5 | ## Code of Conduct 6 | 7 | This project and everyone participating in it is governed by the [Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. 8 | 9 | ## Filing Issues 10 | 11 | Bugs and enhancement suggestions are tracked as GitHub issues. 12 | 13 | #### How Do I Submit A (Good) Bug Report? 14 | 15 | After you've determined which repository your bug is related to and that the issue is still present in the latest version of the master branch, create an issue on that repository and provide the following information: 16 | 17 | - Use a **clear and descriptive title** for the issue to identify the problem. 18 | - Explain which **behavior you expected** to see instead and why. 19 | - Describe the exact **steps to reproduce the problem** in as many details as necessary. 20 | - When providing code samples, please use [code blocks](https://help.github.com/articles/creating-and-highlighting-code-blocks/). 21 | 22 | #### How Do I Submit A (Good) Enhancement Suggestion? 23 | 24 | Instructions are similar to those for bug reports. Please provide the following information: 25 | 26 | - Use a **clear and descriptive title** for the issue to identify the suggestion. 27 | - Provide a **description of the suggested enhancement** in as many details as necessary. 28 | - When providing code samples, please use [code blocks](https://help.github.com/articles/creating-and-highlighting-code-blocks/). 29 | 30 | ## Submitting Pull Requests 31 | 32 | Instructions are similar to those for bug reports. Please provide the following information: 33 | 34 | - Use a **clear and descriptive title** for the pull request. 35 | - Provide a **description of the suggested changes** in as many details as necessary. 36 | - **Document your new code** where necessary. 37 | - Please **refrain from refactoring (unrelated code)** as it makes your pull request easier to review. 38 | - **Create tests for your new code** where necessary. For creating or updating tests, please see the [Test Instructions](./__tests__). 39 | 40 | Before submitting your pull request, please make sure that the following conditions are met: 41 | 42 | - Your new code **adheres to the code style** through running `npm run prettier`. 43 | - Your new code **passes all existing and new tests** through running `npm test`. 44 | - You appended yourself to the **list of contributors** in the [README](./README.md) file. 45 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.setTransform.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | function every(items, callback) { 12 | for (let i = 0; i < items.length; i++) { 13 | if (callback(items[i])) { 14 | continue; 15 | } 16 | return false; 17 | } 18 | return true; 19 | } 20 | 21 | describe('setTransform', () => { 22 | it('should be a function', () => { 23 | expect(typeof ctx.setTransform).toBe('function'); 24 | }); 25 | 26 | it('should be callable', () => { 27 | ctx.setTransform(1, 2, 3, 4, 5, 6); 28 | expect(ctx.setTransform).toBeCalled(); 29 | }); 30 | 31 | it('should validate setTransform input', () => { 32 | [ 33 | [1, 2, 3, 4, 5, 6], 34 | [-1, 2, 3, 4, 5, 6], 35 | [Infinity, null, 'test', 'bad', NaN, 34], 36 | ].forEach((e) => { 37 | ctx.setTransform(1, 0, 0, 1, 0, 0); 38 | ctx.setTransform(...e); 39 | if (every(e, (val) => Number.isFinite(Number(val)))) { 40 | expect(ctx.getTransform()).toEqual(new DOMMatrix(e)); 41 | } else { 42 | expect(ctx.getTransform().isIdentity).toBeTruthy(); 43 | } 44 | }); 45 | }); 46 | 47 | it('should accept a 2d matrix as a valid setTransform parameter', () => { 48 | const m = new DOMMatrix([1, 2, 3, 4, 5, 6]); 49 | ctx.setTransform(m); 50 | expect(ctx.getTransform()).toEqual(new DOMMatrix([1, 2, 3, 4, 5, 6])); 51 | }); 52 | 53 | it("should throw when setTransform doesn't receive valid DOMMatrix", () => { 54 | expect(() => ctx.setTransform({})).toThrow(TypeError); 55 | }); 56 | 57 | it('should accept 0 parameters for the setTransform function (resetTransform)', () => { 58 | ctx.setTransform(1, 2, 3, 4, 5, 6); 59 | expect(ctx.getTransform()).toEqual(new DOMMatrix([1, 2, 3, 4, 5, 6])); 60 | ctx.setTransform(); 61 | expect(ctx.getTransform()).toEqual(new DOMMatrix([1, 0, 0, 1, 0, 0])); 62 | }); 63 | 64 | it('should not throw but return if any value provided to setTransform is not finite', () => { 65 | const identity = new DOMMatrix([1, 0, 0, 1, 0, 0]); 66 | [ 67 | [NaN, 2, 3, 4, 5, 6], 68 | [1, NaN, 3, 4, 5, 6], 69 | [1, 2, NaN, 4, 5, 6], 70 | [1, 2, 3, NaN, 5, 6], 71 | [1, 2, 3, 4, NaN, 6], 72 | [1, 2, 3, 4, 5, NaN], 73 | ].forEach((e) => { 74 | ctx.setTransform(...e); 75 | expect(ctx.getTransform()).toEqual(identity); 76 | }); 77 | }); 78 | 79 | it('should throw if setTransform receives 3-5 parameters', () => { 80 | expect(() => ctx.setTransform(1, 2, 3, 4)).toThrow(TypeError); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /src/mock/createImageBitmap.js: -------------------------------------------------------------------------------- 1 | import ImageBitmap from '../classes/ImageBitmap'; 2 | 3 | export default jest.fn(function createImageBitmap( 4 | img, 5 | sx, 6 | sy, 7 | sWidth, 8 | sHeight, 9 | options 10 | ) { 11 | var length = arguments.length; 12 | return new Promise((resolve, reject) => { 13 | if (length === 0) 14 | return reject( 15 | new TypeError( 16 | "Failed to execute 'createImageBitmap' on 'Window': 1 argument required, but only 0 present." 17 | ) 18 | ); 19 | if (length === 3 || length === 4) 20 | return reject( 21 | new TypeError( 22 | "Failed to execute 'createImageBitmap' on 'Window': Valid arities are: [1, 2, 5, 6], but " + 23 | length + 24 | ' arguments provided.' 25 | ) 26 | ); 27 | let validImage = false; 28 | if (img instanceof HTMLImageElement) validImage = true; 29 | if (img instanceof HTMLVideoElement) validImage = true; 30 | if (img instanceof HTMLCanvasElement) validImage = true; 31 | // checking constructor name is the only reliable way to verify the object's constructing class is "blob-like" 32 | if ( 33 | img instanceof Blob || 34 | (img && img.constructor && img.constructor.name === 'Blob') 35 | ) 36 | validImage = true; 37 | if (img instanceof ImageBitmap) validImage = true; 38 | if (img instanceof ImageData) validImage = true; 39 | if (!validImage) 40 | return reject( 41 | new TypeError( 42 | "Failed to execute 'createImageBitmap' on 'Window': The provided value is not of type '(HTMLImageElement or SVGImageElement or HTMLVideoElement or HTMLCanvasElement or Blob or ImageData or ImageBitmap or OffscreenCanvas)'" 43 | ) 44 | ); 45 | if (length >= 2) { 46 | let index = 6; 47 | if (length === 2) { 48 | index = 2; 49 | options = sx; 50 | } 51 | if (length === 5) options = null; 52 | if (options !== null && options !== void 0) { 53 | if (typeof options !== 'object') 54 | throw new TypeError( 55 | "Failed to execute 'createImageBitmap' on 'Window': parameter " + 56 | index + 57 | " ('options') is not an object." 58 | ); 59 | } 60 | } 61 | 62 | if (length >= 5) { 63 | sWidth = Number(sWidth); 64 | sHeight = Number(sHeight); 65 | if (sWidth === 0 || !Number.isFinite(sWidth)) 66 | return reject(new RangeError('The crop rect width is 0.')); 67 | if (sHeight === 0 || !Number.isFinite(sHeight)) 68 | return reject(new RangeError('The crop rect height is 0.')); 69 | sWidth = Math.abs(sWidth); 70 | sHeight = Math.abs(sHeight); 71 | } else { 72 | sWidth = img.width || 1; 73 | sHeight = img.height || 1; 74 | } 75 | return resolve(new ImageBitmap(sWidth, sHeight)); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /src/classes/ImageData.js: -------------------------------------------------------------------------------- 1 | export default class ImageData { 2 | _width = 0; 3 | _height = 0; 4 | _data = null; 5 | 6 | get width() { 7 | return this._width; 8 | } 9 | 10 | get height() { 11 | return this._height; 12 | } 13 | 14 | get data() { 15 | return this._data; 16 | } 17 | 18 | constructor(arr, w, h) { 19 | if (arguments.length === 2) { 20 | if (arr instanceof Uint8ClampedArray) { 21 | if (arr.length === 0) 22 | throw new RangeError( 23 | 'Source length must be a positive multiple of 4.' 24 | ); 25 | if (arr.length % 4 !== 0) 26 | throw new RangeError( 27 | 'Source length must be a positive multiple of 4.' 28 | ); 29 | 30 | if (!Number.isFinite(w)) 31 | throw new RangeError('The width is zero or not a number.'); 32 | if (w === 0) throw new RangeError('The width is zero or not a number.'); 33 | 34 | this._width = w; 35 | this._height = arr.length / 4 / w; 36 | this._data = arr; 37 | } else { 38 | const width = arr; 39 | const height = w; 40 | 41 | if (!Number.isFinite(height)) 42 | throw new RangeError('The height is zero or not a number.'); 43 | if (height === 0) 44 | throw new RangeError('The height is zero or not a number.'); 45 | if (!Number.isFinite(width)) 46 | throw new RangeError('The width is zero or not a number.'); 47 | if (width === 0) 48 | throw new RangeError('The width is zero or not a number.'); 49 | 50 | this._width = width; 51 | this._height = height; 52 | this._data = new Uint8ClampedArray(width * height * 4); 53 | } 54 | } else if (arguments.length === 3) { 55 | if (!(arr instanceof Uint8ClampedArray)) 56 | throw new TypeError( 57 | 'First argument must be a Uint8ClampedArray when using 3 arguments.' 58 | ); 59 | if (arr.length === 0) 60 | throw new RangeError('Source length must be a positive multiple of 4.'); 61 | if (arr.length % 4 !== 0) 62 | throw new RangeError('Source length must be a positive multiple of 4.'); 63 | 64 | if (!Number.isFinite(h)) 65 | throw new RangeError('The height is zero or not a number.'); 66 | if (h === 0) throw new RangeError('The height is zero or not a number.'); 67 | if (!Number.isFinite(w)) 68 | throw new RangeError('The width is zero or not a number.'); 69 | if (w === 0) throw new RangeError('The width is zero or not a number.'); 70 | 71 | if (arr.length !== w * h * 4) 72 | throw new RangeError( 73 | "Source doesn'n contain the exact number of pixels needed." 74 | ); 75 | 76 | this._width = w; 77 | this._height = h; 78 | this._data = arr; 79 | } else { 80 | throw new TypeError('Wrong number of arguments provided.'); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.createImagePattern.js: -------------------------------------------------------------------------------- 1 | let canvas; 2 | let ctx; 3 | 4 | beforeEach(() => { 5 | canvas = document.createElement('canvas'); 6 | ctx = canvas.getContext('2d'); 7 | canvas.width = 400; 8 | canvas.height = 300; 9 | }); 10 | 11 | describe('createImagePattern', () => { 12 | it('should createImagePatterns', () => { 13 | const img = new Image(); 14 | img.src = 'http://some-domain.com/my-image.png'; 15 | const result = ctx.createPattern(img, 'no-repeat'); 16 | expect(result).toBeInstanceOf(CanvasPattern); 17 | }); 18 | 19 | it("shouldn't create image patterns when argument length is 1", () => { 20 | const img = new Image(); 21 | img.src = 'http://some-domain.com/my-image.png'; 22 | expect(() => ctx.createPattern(img)).toThrow(TypeError); 23 | }); 24 | 25 | it("shouldn't create image patterns when second argument is undefined", () => { 26 | const img = new Image(); 27 | img.src = 'http://some-domain.com/my-image.png'; 28 | expect(() => ctx.createPattern(img, void 0)).toThrow(TypeError); 29 | }); 30 | 31 | it('should create image patterns when second argument is null', () => { 32 | const img = new Image(); 33 | img.src = 'http://some-domain.com/my-image.png'; 34 | expect(ctx.createPattern(img, null)).toBeInstanceOf(CanvasPattern); 35 | }); 36 | 37 | it('should create image patterns when second argument is empty string', () => { 38 | const img = new Image(); 39 | img.src = 'http://some-domain.com/my-image.png'; 40 | expect(ctx.createPattern(img, '')).toBeInstanceOf(CanvasPattern); 41 | }); 42 | 43 | it("shouldn't create imagePattern when image is not valid", () => { 44 | expect(() => ctx.createPattern(null, 'repeat')).toThrow(); 45 | }); 46 | 47 | it('should create a pattern when image is Video', () => { 48 | const video = document.createElement('video'); 49 | expect(ctx.createPattern(video, 'repeat')).toBeInstanceOf(CanvasPattern); 50 | }); 51 | 52 | it('should create a pattern when image is Canvas', () => { 53 | const canvas = document.createElement('canvas'); 54 | expect(ctx.createPattern(canvas, 'repeat')).toBeInstanceOf(CanvasPattern); 55 | }); 56 | 57 | it('should create a pattern when image is ImageBitmap', () => { 58 | expect( 59 | ctx.createPattern(new ImageBitmap(400, 300), 'repeat') 60 | ).toBeInstanceOf(CanvasPattern); 61 | }); 62 | 63 | it('should not create a pattern if the image bitmap is closed', () => { 64 | const bmp = new ImageBitmap(400, 300); 65 | bmp.close(); 66 | expect(() => ctx.createPattern(bmp, 'repeat')).toThrow(DOMException); 67 | }); 68 | 69 | it('should create a valid pattern for all repeat types', () => { 70 | const image = new Image(); 71 | image.src = 'test/myImage.jpg'; 72 | 73 | ['repeat', 'repeat-x', 'repeat-y', 'no-repeat'].forEach((e) => { 74 | expect(ctx.createPattern(image, e)).toBeInstanceOf(CanvasPattern); 75 | }); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | - Using welcoming and inclusive language 12 | - Being respectful of differing viewpoints and experiences 13 | - Gracefully accepting constructive criticism 14 | - Focusing on what is best for the community 15 | - Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | - Trolling, insulting/derogatory comments, and personal or political attacks 21 | - Public or private harassment 22 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | - Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [https://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: https://contributor-covenant.org 46 | [version]: https://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.__getPath.js: -------------------------------------------------------------------------------- 1 | let ctx; 2 | beforeEach(() => { 3 | // get a new context each test 4 | ctx = document.createElement('canvas').getContext('2d'); 5 | }); 6 | 7 | const path = new Path2D(); 8 | path.arc(100, 101, 10, 0, Math.PI * 2); 9 | 10 | afterEach(() => { 11 | const drawCalls = ctx.__getPath(); 12 | expect(drawCalls).toMatchSnapshot(); 13 | }); 14 | 15 | describe('__getPath', () => { 16 | it('should have a path item when arc is called', () => { 17 | ctx.arc(1, 2, 3, 4, 5, true); 18 | }); 19 | 20 | it('should not have a path item when arc is called with a non-finite number', () => { 21 | ctx.arc(NaN, 2, 3, 4, 5, false); 22 | }); 23 | 24 | it('should have a path item when arcTo is called', () => { 25 | ctx.arcTo(1, 2, 3, 4, 5); 26 | }); 27 | 28 | it('should not have a path item when arcTo is called with a non-finite number', () => { 29 | ctx.arcTo(NaN, 2, 3, 4, 5); 30 | }); 31 | 32 | it('should reset the path with beginPath', () => { 33 | ctx.arc(1, 2, 3, 4, 5, true); 34 | ctx.arc(1, 2, 3, 4, 5, true); 35 | ctx.arc(1, 2, 3, 4, 5, true); 36 | ctx.arc(1, 2, 3, 4, 5, true); 37 | ctx.beginPath(); 38 | }); 39 | 40 | it('should have a path item when bezierCurveTo is called', () => { 41 | ctx.bezierCurveTo(1, 2, 3, 4, 5, 6); 42 | }); 43 | 44 | it('should not have a path item when bezierCurveTo is called with a non-finite number', () => { 45 | ctx.bezierCurveTo(NaN, 2, 3, 4, 5, 6); 46 | }); 47 | 48 | it('should have a path item when clip is called', () => { 49 | ctx.rect(1, 2, 3, 4); 50 | ctx.clip(); 51 | }); 52 | 53 | it('should have a path item when clip is called with a fillRule', () => { 54 | ctx.rect(1, 2, 3, 4); 55 | ctx.clip('evenodd'); 56 | }); 57 | 58 | it('should have a path item when clip is called with a path', () => { 59 | ctx.clip(path); 60 | }); 61 | 62 | it('should have a path item when clip is called with a path', () => { 63 | ctx.clip(path, 'evenodd'); 64 | }); 65 | 66 | it('should have a path item when closePath is called', () => { 67 | ctx.closePath(); 68 | }); 69 | 70 | it('should have a path item when ellipse is called', () => { 71 | ctx.ellipse(1, 2, 3, 4, 5, 6, 7, true); 72 | }); 73 | 74 | it('should not have a path item when ellipse is called with non-finite numbers', () => { 75 | ctx.ellipse(NaN, 2, 3, 4, 5, 6, 7, false); 76 | }); 77 | 78 | it('should have a path item when lineTo is called', () => { 79 | ctx.lineTo(1, 2); 80 | }); 81 | 82 | it('should not have a path item when lineTo is called with non-finite values', () => { 83 | ctx.lineTo(NaN, 0); 84 | }); 85 | 86 | it('should have a path item when moveTo is called', () => { 87 | ctx.moveTo(1, 2); 88 | }); 89 | 90 | it('should not have a path item when moveTo is called with non-finite values', () => { 91 | ctx.moveTo(NaN, 1); 92 | }); 93 | 94 | it('should have a path item when quadraticCurveTo is called', () => { 95 | ctx.quadraticCurveTo(1, 2, 3, 4); 96 | }); 97 | 98 | it('should not have a path item when quadraticCurveTo is called with non-finite values', () => { 99 | ctx.quadraticCurveTo(NaN, 2, 3, 4); 100 | }); 101 | 102 | it('should have a path item when rect is called', () => { 103 | ctx.rect(1, 2, 3, 4); 104 | }); 105 | 106 | it('should not have a path item when rect is called with non-finite values', () => { 107 | ctx.rect(NaN, 2, 3, 4); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | export function setupJestCanvasMock(window?: Window) {} 2 | 3 | export interface CanvasRenderingContext2DEvent { 4 | /** 5 | * This is the type of canvas event that occurred. 6 | */ 7 | type: string; 8 | /** 9 | * This is a six element array that contains the current state of the canvas `currentTransform` 10 | * value. 11 | */ 12 | transform: [number, number, number, number, number, number]; 13 | /** 14 | * These are the relevant properties related to this canvas event. 15 | */ 16 | props: { 17 | [key: string]: any; 18 | }; 19 | } 20 | 21 | declare global { 22 | interface CanvasRenderingContext2D { 23 | /** 24 | * Get all the events associated with this CanvasRenderingContext2D object. 25 | * 26 | * This method cannot be used in a production environment, only with `jest` using 27 | * `jest-canvas-mock` and should only be used for testing. 28 | * 29 | * @example 30 | * expect(ctx.__getEvents()).toMatchSnapshot(); 31 | */ 32 | __getEvents(): CanvasRenderingContext2DEvent[]; 33 | 34 | /** 35 | * Clear all the events associated with this CanvasRenderingContext2D object. 36 | * 37 | * This method cannot be used in a production environment, only with `jest` using 38 | * `jest-canvas-mock` and should only be used for testing. 39 | * 40 | * @example 41 | * ctx.__clearEvents()); 42 | * expect(ctx.__getEvents()).toBe([]); 43 | */ 44 | __clearEvents(): void; 45 | 46 | /** 47 | * Get all the successful draw calls associated with this CanvasRenderingContext2D object. 48 | * 49 | * This method cannot be used in a production environment, only with `jest` using 50 | * `jest-canvas-mock` and should only be used for testing. 51 | * 52 | * @example 53 | * expect(ctx.__getDrawCalls()).toMatchSnapshot(); 54 | */ 55 | __getDrawCalls(): CanvasRenderingContext2DEvent[]; 56 | 57 | /** 58 | * Clear all the successful draw calls associated with this CanvasRenderingContext2D object. 59 | * 60 | * This method cannot be used in a production environment, only with `jest` using 61 | * `jest-canvas-mock` and should only be used for testing. 62 | * 63 | * @example 64 | * ctx.__clearDrawCalls()); 65 | * expect(ctx.__getDrawCalls()).toBe([]); 66 | */ 67 | __clearDrawCalls(): void; 68 | 69 | /** 70 | * Get the current path associated with this CanvasRenderingContext2D object. 71 | * 72 | * This method cannot be used in a production environment, only with `jest` using 73 | * `jest-canvas-mock` and should only be used for testing. 74 | * 75 | * @example 76 | * expect(ctx.__getPath()).toMatchSnapshot(); 77 | */ 78 | __getPath(): CanvasRenderingContext2DEvent[]; 79 | 80 | /** 81 | * Clears the current path associated with this CanvasRenderingContext2D object. 82 | * 83 | * This method cannot be used in a production environment, only with `jest` using 84 | * `jest-canvas-mock` and should be only used for testing. 85 | */ 86 | __clearPath(): void; 87 | 88 | /** 89 | * Obtains the current clipping path. 90 | * 91 | * This method cannot be used in a production environment, only with `jest` using 92 | * `jest-canvas-mock` and should be only used for testing. 93 | */ 94 | __getClippingRegion(): CanvasRenderingContext2DEvent[]; 95 | } 96 | 97 | interface WeblGLRenderingContext { 98 | getExtension(ext: string): any; 99 | getParameter(param: string): any; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/window.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hustcc 17/12/25. 3 | * Contract: i@hust.cc 4 | */ 5 | 6 | import Path2D from './classes/Path2D'; 7 | import CanvasGradient from './classes/CanvasGradient'; 8 | import CanvasPattern from './classes/CanvasPattern'; 9 | import CanvasRenderingContext2D from './classes/CanvasRenderingContext2D'; 10 | import DOMMatrix from './classes/DOMMatrix'; 11 | import ImageData from './classes/ImageData'; 12 | import TextMetrics from './classes/TextMetrics'; 13 | import ImageBitmap from './classes/ImageBitmap'; 14 | import mockPrototype from './mock/prototype'; 15 | import createImageBitmap from './mock/createImageBitmap'; 16 | import Image from './classes/Image'; 17 | import WebGLRenderingContext from './classes/WebGLRenderingContext'; 18 | 19 | export default (win) => { 20 | const d = win.document; 21 | const f = win.document.createElement; 22 | 23 | // jsdom@11.6.2 || jest@^22.0.0, console.error in Function getContext(); 24 | // https://github.com/jsdom/jsdom/blob/4c7698f760fc64f20b2a0ddff450eddbdd193176/lib/jsdom/living/nodes/HTMLCanvasElement-impl.js#L55-L58 25 | // console.error will make ci error. 26 | // try { 27 | // // get the context 2d. 28 | // const ctx = d.createElement('canvas').getContext('2d'); 29 | // 30 | // // if canvas and context2d all exist, means mock is not needed. 31 | // if (ctx) { 32 | // console.warn('Context 2d of canvas is exist! No need to mock'); 33 | // return win; 34 | // } 35 | // } catch (_) { 36 | // // catch the throw `Error: Not implemented: HTMLCanvasElement.prototype.getContext` 37 | // // https://github.com/jsdom/jsdom/blob/4c7698f760fc64f20b2a0ddff450eddbdd193176/lib/jsdom/living/nodes/HTMLCanvasElement-impl.js 38 | // // when throw error, means mock is needed. 39 | // // code continue 40 | // } 41 | // if ctx not exist, mock it. 42 | // just mock canvas creator. 43 | /* 44 | win.document.createElement = param => param.toString().toLowerCase() === 'canvas' 45 | ? createCanvas('canvas') 46 | : f.call(d, param); 47 | */ 48 | // if not exist, then mock it. 49 | if (!win.Path2D) win.Path2D = Path2D; 50 | if (!win.CanvasGradient) win.CanvasGradient = CanvasGradient; 51 | if (!win.CanvasPattern) win.CanvasPattern = CanvasPattern; 52 | if (!win.CanvasRenderingContext2D) 53 | win.CanvasRenderingContext2D = CanvasRenderingContext2D; 54 | if (!win.DOMMatrix) win.DOMMatrix = DOMMatrix; 55 | if (!win.ImageData) win.ImageData = ImageData; 56 | if (!win.TextMetrics) win.TextMetrics = TextMetrics; 57 | if (!win.ImageBitmap) win.ImageBitmap = ImageBitmap; 58 | if (!win.createImageBitmap) win.createImageBitmap = createImageBitmap; 59 | 60 | if (!win.Image) win.Image = Image; 61 | if (!win.HTMLImageElement) win.HTMLImageElement = Image; 62 | if (!win.HTMLVideoElement) win.HTMLVideoElement = Image; 63 | 64 | // WebGL 1.0 65 | if (!win.WebGLRenderingContext) 66 | win.WebGLRenderingContext = WebGLRenderingContext; 67 | if (!win.WebGLActiveInfo) win.WebGLActiveInfo = function () {}; 68 | if (!win.WebGLBuffer) win.WebGLBuffer = function () {}; 69 | if (!win.WebGLContextEvent) win.WebGLContextEvent = function () {}; 70 | if (!win.WebGLFramebuffer) win.WebGLFramebuffer = function () {}; 71 | if (!win.WebGLProgram) win.WebGLProgram = function () {}; 72 | if (!win.WebGLQuery) win.WebGLQuery = function () {}; 73 | if (!win.WebGLRenderbuffer) win.WebGLRenderbuffer = function () {}; 74 | if (!win.WebGLShader) win.WebGLShader = function () {}; 75 | if (!win.WebGLShaderPrecisionFormat) 76 | win.WebGLShaderPrecisionFormat = function () {}; 77 | if (!win.WebGLTexture) win.WebGLTexture = function () {}; 78 | if (!win.WebGLUniformLocation) win.WebGLUniformLocation = function () {}; 79 | 80 | mockPrototype(); 81 | 82 | return win; 83 | }; 84 | -------------------------------------------------------------------------------- /__tests__/classes/CanvasRenderingContext2D.__getDrawCalls.js: -------------------------------------------------------------------------------- 1 | let ctx; 2 | beforeEach(() => { 3 | // get a new context each test 4 | ctx = document.createElement('canvas').getContext('2d'); 5 | }); 6 | 7 | const img = new Image(); 8 | img.src = 'https://placekitten.com/400/300'; 9 | img.width = 400; 10 | img.height = 300; 11 | 12 | const path = new Path2D(); 13 | path.arc(100, 101, 10, 0, Math.PI * 2); 14 | 15 | afterEach(() => { 16 | const drawCalls = ctx.__getDrawCalls(); 17 | expect(drawCalls).toMatchSnapshot(); 18 | }); 19 | 20 | describe('__getDrawCalls', () => { 21 | it('should have a draw call when clearRect is called', () => { 22 | ctx.clearRect(1, 2, 3, 4); 23 | }); 24 | 25 | it('should not have a draw call when clearRect is passed bad values', () => { 26 | ctx.clearRect(NaN, 1, 2, 3); 27 | }); 28 | 29 | it('should have a draw call when fillRect is called', () => { 30 | ctx.fillRect(1, 2, 3, 4); 31 | }); 32 | 33 | it('should not have a draw call when fillRect is passed bad values', () => { 34 | ctx.fillRect(NaN, 1, 2, 3); 35 | }); 36 | 37 | it('should have a draw call when strokeRect is called', () => { 38 | ctx.strokeRect(1, 2, 3, 4); 39 | }); 40 | 41 | it('should not have a draw call when strokeRect is passed bad values', () => { 42 | ctx.strokeRect(NaN, 1, 2, 3); 43 | }); 44 | 45 | it('should have a draw call when drawImage is called', () => { 46 | ctx.drawImage(img, 0, 0); 47 | }); 48 | 49 | it('should not have a draw call when drawImage is called with non-finite numbers', () => { 50 | ctx.drawImage(img, NaN, 0); 51 | }); 52 | 53 | it('should have a draw call when drawImage is called with size parameters', () => { 54 | ctx.drawImage(img, 0, 0, 100, 100); 55 | }); 56 | 57 | it('should not have a draw call when drawImage is called with non-finite numbers and size parameters', () => { 58 | ctx.drawImage(img, NaN, 0, 100, 100); 59 | }); 60 | 61 | it('should have a draw call when drawImage is called with source parameters', () => { 62 | ctx.drawImage(img, 0, 0, 100, 100, 0, 0, 200, 200); 63 | }); 64 | 65 | it('should not have a draw call when drawImage is called with non-finite numbers and source parameters', () => { 66 | ctx.drawImage(img, NaN, 0, 100, 100, 0, 0, 200, 200); 67 | }); 68 | 69 | it('should have a draw call when fill() is called', () => { 70 | ctx.beginPath(); 71 | ctx.arc(100, 101, 10, 0, Math.PI * 2); 72 | ctx.fill(); 73 | }); 74 | 75 | it('should have a draw call when fill() is called with a fillRule', () => { 76 | ctx.beginPath(); 77 | ctx.arc(100, 101, 10, 0, Math.PI * 2); 78 | ctx.fill('evenodd'); 79 | }); 80 | 81 | it('should have a draw call when using a path', () => { 82 | ctx.fill(path); 83 | }); 84 | 85 | it('should have a draw call when fillRule is valid', () => { 86 | ctx.fill(path, 'evenodd'); 87 | }); 88 | 89 | it('should have a draw call when fillText is valid', () => { 90 | ctx.fillText('hello world', 0, 0); 91 | }); 92 | 93 | it('should have a draw call when fillText has valid maxWidth', () => { 94 | ctx.fillText('hello world', 0, 0, 100); 95 | }); 96 | 97 | it('should not have a draw call when fillText is not valid', () => { 98 | ctx.fillText('hello world', 0, NaN); 99 | }); 100 | 101 | it('should not have a draw call when fillText is not valid', () => { 102 | ctx.fillText('hello world', 0, 0, NaN); 103 | }); 104 | 105 | it('should have a draw call when strokeText is valid', () => { 106 | ctx.strokeText('hello world', 0, 0); 107 | }); 108 | 109 | it('should have a draw call when strokeText has valid maxWidth', () => { 110 | ctx.strokeText('hello world', 0, 0, 100); 111 | }); 112 | 113 | it('should not have a draw call when strokeText is not valid', () => { 114 | ctx.strokeText('hello world', 0, NaN); 115 | }); 116 | 117 | it('should not have a draw call when strokeText is not valid', () => { 118 | ctx.strokeText('hello world', 0, 0, NaN); 119 | }); 120 | 121 | it('should have a draw call when stroke is called', () => { 122 | ctx.beginPath(); 123 | ctx.arc(100, 101, 10, 0, Math.PI * 2); 124 | ctx.stroke(); 125 | }); 126 | 127 | it('should have a draw call when stroke is called with a path', () => { 128 | ctx.stroke(path); 129 | }); 130 | }); 131 | -------------------------------------------------------------------------------- /src/mock/prototype.js: -------------------------------------------------------------------------------- 1 | import CanvasRenderingContext2D from '../classes/CanvasRenderingContext2D'; 2 | import WebGLRenderingContext from '../classes/WebGLRenderingContext'; 3 | 4 | export default function mockPrototype() { 5 | /** 6 | * This weakmap is designed to contain all of the generated canvas contexts. It's keys are the 7 | * jsdom canvases obtained by using the `this` keyword inside the `#getContext('2d')` function 8 | * call. It's values are the generated `CanvasRenderingContext2D` objects. 9 | */ 10 | const generatedContexts = new WeakMap(); 11 | /** 12 | * Overrides getContext. Every test run will create a new function that overrides the current 13 | * value of getContext. It attempts to preserve the original getContext function by storing it on 14 | * the callback as a property. 15 | */ 16 | function getContext(type, options) { 17 | if (type === '2d') { 18 | /** 19 | * Contexts must be idempotent. Once they are generated, they should be returned when 20 | * getContext() is called on the same canvas object multiple times. 21 | */ 22 | if (generatedContexts.has(this)) return generatedContexts.get(this); 23 | const ctx = new CanvasRenderingContext2D(this, options); 24 | generatedContexts.set(this, ctx); 25 | return ctx; 26 | } else if (type === 'webgl' || type === 'experimental-webgl') { 27 | if (generatedContexts.has(this)) return generatedContexts.get(this); 28 | const ctx = new WebGLRenderingContext(this, options); 29 | generatedContexts.set(this, ctx); 30 | return ctx; 31 | } 32 | return getContext.internal.call(this, type, options); 33 | } 34 | 35 | getContext.internal = HTMLCanvasElement.prototype.getContext; 36 | 37 | HTMLCanvasElement.prototype.getContext = getContext; 38 | 39 | /** 40 | * This function technically throws SecurityError at runtime, but it cannot be mocked, because 41 | * we don't know if the canvas is tainted. These kinds of errors will be silent. 42 | */ 43 | const toBlobOverride = jest.fn(function toBlobOverride(callback, mimetype) { 44 | if (arguments.length < 1) 45 | throw new TypeError( 46 | "Failed to execute 'toBlob' on 'HTMLCanvasElement': 1 argument required, but only 0 present." 47 | ); 48 | if (typeof callback !== 'function') 49 | throw new TypeError( 50 | "Failed to execute 'toBlob' on 'HTMLCanvasElement': The callback provided as parameter 1 is not a function." 51 | ); 52 | 53 | /** 54 | * Mime type must be image/jpeg or image/webp exactly for the browser to accept it, otherwise 55 | * it's image/png. 56 | */ 57 | switch (mimetype) { 58 | case 'image/webp': 59 | break; 60 | case 'image/jpeg': 61 | break; 62 | default: 63 | mimetype = 'image/png'; 64 | } 65 | 66 | /** 67 | * This section creates a blob of size width * height * 4. This is not actually valid, because 68 | * jpeg size is variable, and so is png. TODO: Is there a better way to do this? 69 | */ 70 | const length = this.width * this.height * 4; 71 | const data = new Uint8Array(length); 72 | const blob = new window.Blob([data], { type: mimetype }); 73 | setTimeout(() => callback(blob), 0); 74 | }); 75 | 76 | if (!jest.isMockFunction(HTMLCanvasElement.prototype.toBlob)) { 77 | toBlobOverride.internal = HTMLCanvasElement.prototype.toBlob; 78 | } else { 79 | toBlobOverride.internal = HTMLCanvasElement.prototype.toBlob.internal; 80 | } 81 | HTMLCanvasElement.prototype.toBlob = toBlobOverride; 82 | 83 | /** 84 | * This section creates a dataurl with a validated mime type. This is not actually valid, because 85 | * jpeg size is variable, and so is png. TODO: Is there a better way to do this? 86 | */ 87 | const toDataURLOverride = jest.fn(function toDataURLOverride( 88 | type, 89 | encoderOptions 90 | ) { 91 | switch (type) { 92 | case 'image/jpeg': 93 | break; 94 | case 'image/webp': 95 | break; 96 | default: 97 | type = 'image/png'; 98 | } 99 | 100 | /** 101 | * This is the smallest valid data url I could generate. 102 | */ 103 | return 'data:' + type + ';base64,00'; 104 | }); 105 | 106 | if (!jest.isMockFunction(HTMLCanvasElement.prototype.toDataURL)) { 107 | toDataURLOverride.internal = HTMLCanvasElement.prototype.toDataURL; 108 | } else { 109 | toDataURLOverride.internal = HTMLCanvasElement.prototype.toDataURL.internal; 110 | } 111 | HTMLCanvasElement.prototype.toDataURL = toDataURLOverride; 112 | } 113 | -------------------------------------------------------------------------------- /__tests__/mock/prototype.js: -------------------------------------------------------------------------------- 1 | import WebGLRenderingContext from '../../src/classes/WebGLRenderingContext'; 2 | 3 | let canvas; 4 | let otherCanvas; 5 | 6 | beforeEach(() => { 7 | canvas = document.createElement('canvas'); 8 | otherCanvas = document.createElement('canvas'); 9 | }); 10 | 11 | describe('mock', () => { 12 | it('context creation of type 2d returns CanvasRenderingContext2D', () => { 13 | const ctx = canvas.getContext('2d'); 14 | expect(ctx).toBeInstanceOf(CanvasRenderingContext2D); 15 | }); 16 | 17 | it('context creation of type webgl returns WebGLRenderingContext', () => { 18 | const ctx = canvas.getContext('webgl'); 19 | expect(ctx).toBeInstanceOf(WebGLRenderingContext); 20 | }); 21 | 22 | it('context creation of type experimental-webgl returns WebGLRenderingContext', () => { 23 | const ctx = canvas.getContext('experimental-webgl'); 24 | expect(ctx).toBeInstanceOf(WebGLRenderingContext); 25 | }); 26 | 27 | it('should have a toBlob function', () => { 28 | expect(typeof canvas.toBlob).toBe('function'); 29 | }); 30 | 31 | it('should expect toBlob to be callable', () => { 32 | canvas.toBlob((e) => {}); 33 | expect(canvas.toBlob).toBeCalled(); 34 | }); 35 | 36 | it('should expect toBlob to return Blob', () => { 37 | return new Promise((resolve, reject) => { 38 | canvas.toBlob((e) => { 39 | var ex; 40 | try { 41 | expect(e).toBeInstanceOf(window.Blob); 42 | } catch (ex) { 43 | return reject(ex); 44 | } 45 | resolve(); 46 | }); 47 | }); 48 | }); 49 | 50 | it('should throw if toBlob is provided less than 1 argument', () => { 51 | expect(() => canvas.toBlob()).toThrow(TypeError); 52 | }); 53 | 54 | it('should throw if toBlob is provided with no callback', () => { 55 | expect(() => canvas.toBlob(1)).toThrow(TypeError); 56 | }); 57 | 58 | it('should accept image/jpeg', () => { 59 | return new Promise((resolve, reject) => { 60 | canvas.toBlob((e) => { 61 | var ex; 62 | try { 63 | expect(e.type).toBe('image/jpeg'); 64 | } catch (ex) { 65 | return reject(ex); 66 | } 67 | resolve(); 68 | }, 'image/jpeg'); 69 | }); 70 | }); 71 | 72 | it('should accept image/webp', () => { 73 | return new Promise((resolve, reject) => { 74 | canvas.toBlob((e) => { 75 | var ex; 76 | try { 77 | expect(e.type).toBe('image/webp'); 78 | } catch (ex) { 79 | return reject(ex); 80 | } 81 | resolve(); 82 | }, 'image/webp'); 83 | }); 84 | }); 85 | 86 | it('should have toDataURL function', () => { 87 | expect(typeof canvas.toDataURL).toBe('function'); 88 | }); 89 | 90 | it('should expect canvas toDataURL to be callable', () => { 91 | canvas.toDataURL(); 92 | expect(canvas.toDataURL).toBeCalled(); 93 | }); 94 | 95 | it('should expect canvas toDataURL to always return string', () => { 96 | expect(canvas.toDataURL()).toBe('data:image/png;base64,00'); 97 | }); 98 | 99 | it('should accept image/jpeg', () => { 100 | expect(canvas.toDataURL('image/jpeg')).toMatch(/image\/jpeg/); 101 | }); 102 | 103 | it('should accept image/webp', () => { 104 | expect(canvas.toDataURL('image/webp')).toMatch(/image\/webp/); 105 | }); 106 | 107 | /** 108 | * This test is very special, because it helps increase the code coverage to 100%. It patches 109 | * console.error to suppress calls to console.error, sets an internal dataset value to force the 110 | * getContext() function to call it's internal getContext() provided by jsdom. 111 | */ 112 | it('should call internal function if "canvas" is installed', () => { 113 | const error = console.error; 114 | console.error = () => void 0; 115 | canvas.dataset.internalRequireTest = true; 116 | canvas.getContext('random'); 117 | console.error = error; 118 | }); 119 | 120 | it('should return the same context if getContext("2d") is called twice', () => { 121 | const first = canvas.getContext('2d'); 122 | const second = canvas.getContext('2d'); 123 | expect(first).toBe(second); 124 | }); 125 | 126 | it('should return the same context if getContext("webgl") is called twice', () => { 127 | const first = canvas.getContext('webgl'); 128 | const second = canvas.getContext('webgl'); 129 | expect(first).toBe(second); 130 | }); 131 | 132 | it('should return the same context if getContext("experimental-webgl") is called twice', () => { 133 | const first = canvas.getContext('experimental-webgl'); 134 | const second = canvas.getContext('experimental-webgl'); 135 | expect(first).toBe(second); 136 | }); 137 | }); 138 | -------------------------------------------------------------------------------- /__tests__/classes/ImageBitmap.js: -------------------------------------------------------------------------------- 1 | var img = new Image(); 2 | img.src = 'test.jpg'; 3 | 4 | /** 5 | * If a promise is expected to reject, then this function, when passed to the `.then()` function, 6 | * will throw -1 instead of the expected error *first*. 7 | */ 8 | function throwError() { 9 | throw -1; 10 | } 11 | 12 | function expectTypeError(e) { 13 | expect(e).toBeInstanceOf(TypeError); 14 | } 15 | 16 | function expectRangeError(e) { 17 | expect(e).toBeInstanceOf(RangeError); 18 | } 19 | 20 | function expectImageBitmap(e) { 21 | expect(e).toBeInstanceOf(ImageBitmap); 22 | } 23 | 24 | describe('image bitmaps', () => { 25 | it('should be a function', () => { 26 | expect(typeof createImageBitmap).toBe('function'); 27 | }); 28 | 29 | it('should be callable', () => { 30 | createImageBitmap(img); 31 | expect(createImageBitmap).toBeCalled(); 32 | }); 33 | 34 | it('should return a promise', () => { 35 | expect(createImageBitmap(img)).toBeInstanceOf(Promise); 36 | }); 37 | 38 | it('should resolve to ImageBitmap', () => { 39 | return createImageBitmap(img).then(expectImageBitmap); 40 | }); 41 | 42 | it('should resolve to a type error if no parameters are passed to it', () => { 43 | return createImageBitmap().then(throwError).catch(expectTypeError); 44 | }); 45 | 46 | it('should reject non images', () => { 47 | return Promise.all( 48 | [0, null, void 0, '', 'test', window, Infinity, document].map((e) => 49 | createImageBitmap(e).then(throwError).catch(expectTypeError) 50 | ) 51 | ); 52 | }); 53 | 54 | it('should reject if second parameter is not an object', () => { 55 | return Promise.all( 56 | ['', 0, NaN, Infinity].map((e) => { 57 | return createImageBitmap(img, e) 58 | .then(throwError) 59 | .catch(expectTypeError); 60 | }) 61 | ); 62 | }); 63 | 64 | it('should throw if arity is 3 or 4', () => { 65 | return Promise.all([ 66 | createImageBitmap(img, 1, 2).then(throwError).catch(expectTypeError), 67 | createImageBitmap(img, 1, 2, 3).then(throwError).catch(expectTypeError), 68 | ]); 69 | }); 70 | 71 | it('should accept 5 parameters if the last 4 parameters are numbers', () => { 72 | return createImageBitmap(img, 1, 2, 3, 4).then(expectImageBitmap); 73 | }); 74 | 75 | it('should throw if width or height is not finite or 0', () => { 76 | return Promise.all([ 77 | createImageBitmap(img, 1, 2, NaN, 3) 78 | .then(throwError) 79 | .catch(expectRangeError), 80 | createImageBitmap(img, 1, 2, 0, 3) 81 | .then(throwError) 82 | .catch(expectRangeError), 83 | createImageBitmap(img, 1, 2, 3, NaN) 84 | .then(throwError) 85 | .catch(expectRangeError), 86 | createImageBitmap(img, 1, 2, 3, 0) 87 | .then(throwError) 88 | .catch(expectRangeError), 89 | ]); 90 | }); 91 | 92 | it('should throw if last parameter is not object if source rect is provided', () => { 93 | return Promise.all( 94 | ['', 0, NaN, Infinity].map((e) => 95 | createImageBitmap(img, 1, 2, 3, 4, e) 96 | .then(throwError) 97 | .catch(expectTypeError) 98 | ) 99 | ); 100 | }); 101 | 102 | it('should have a close function', () => { 103 | return createImageBitmap(img).then((e) => { 104 | expect(typeof e.close).toBe('function'); 105 | }); 106 | }); 107 | 108 | it('should have a callable close function', () => { 109 | return createImageBitmap(img).then((e) => { 110 | e.close(); 111 | expect(e.close).toBeCalled(); 112 | }); 113 | }); 114 | 115 | it('should close the bitmap', () => { 116 | return createImageBitmap(img).then((e) => { 117 | e.close(); 118 | expect(e.width).toBe(0); 119 | expect(e.height).toBe(0); 120 | expect(e._closed).toBe(true); 121 | }); 122 | }); 123 | 124 | it('should create image bitmaps from valid sources', () => { 125 | return Promise.all( 126 | [ 127 | document.createElement('img'), 128 | new Image(), 129 | document.createElement('canvas'), 130 | document.createElement('video'), 131 | new Blob([new Uint8Array(1)]), 132 | new ImageData(400, 300), 133 | new ImageBitmap(100, 100), // this is just to verify class input 134 | ].map((e) => createImageBitmap(e).then(expectImageBitmap)) 135 | ); 136 | }); 137 | 138 | it('should reject if sixth parameter is not an object', () => { 139 | return Promise.all( 140 | ['', 0, NaN, Infinity].map((e) => { 141 | return createImageBitmap(img, 1, 2, 3, 4, e) 142 | .then(throwError) 143 | .catch(expectTypeError); 144 | }) 145 | ); 146 | }); 147 | 148 | it('should accept if sixth parameter is null, undefined, or object', () => { 149 | return Promise.all( 150 | [null, void 0, {}].map((e) => { 151 | return createImageBitmap(img, 1, 2, 3, 4, e).then(expectImageBitmap); 152 | }) 153 | ); 154 | }); 155 | }); 156 | -------------------------------------------------------------------------------- /__tests__/classes/ImageData.js: -------------------------------------------------------------------------------- 1 | describe('ImageData', () => { 2 | it('should throw if less than 2 arguments are provided', () => { 3 | expect(() => new ImageData()).toThrow(TypeError); 4 | }); 5 | 6 | it('should throw if more than 3 arguments are provided', () => { 7 | expect( 8 | () => new ImageData(new Uint8ClampedArray([0, 0, 0, 1]), 1, 1, 1) 9 | ).toThrow(TypeError); 10 | }); 11 | 12 | describe('new ImageData(width, height)', () => { 13 | it('should construct an ImageData object', () => { 14 | const d = new ImageData(100, 200); 15 | expect(d).toBeInstanceOf(ImageData); 16 | }); 17 | 18 | it('should construct a Uint8ClampedArray', () => { 19 | const d = new ImageData(100, 200); 20 | expect(d.data).toBeInstanceOf(Uint8ClampedArray); 21 | }); 22 | 23 | it('should construct a Uint8ClampedArray of proper size', () => { 24 | const d = new ImageData(100, 200); 25 | expect(d.data.length).toBe(100 * 200 * 4); 26 | }); 27 | 28 | it('should set the width and height', () => { 29 | const d = new ImageData(100, 200); 30 | expect(d.width).toBe(100); 31 | expect(d.height).toBe(200); 32 | }); 33 | 34 | it('should throw if width is not finite', () => { 35 | expect(() => new ImageData(Infinity, 100)).toThrow(RangeError); 36 | }); 37 | 38 | it('should throw if width is 0', () => { 39 | expect(() => new ImageData(0, 100)).toThrow(RangeError); 40 | }); 41 | 42 | it('should throw if height is not finite', () => { 43 | expect(() => new ImageData(100, Infinity)).toThrow(RangeError); 44 | }); 45 | 46 | it('should throw if height is 0', () => { 47 | expect(() => new ImageData(100, 0)).toThrow(RangeError); 48 | }); 49 | }); 50 | 51 | describe('new ImageData(array, width)', () => { 52 | it('should construct an ImageData object with the right inferred height', () => { 53 | const data = new Uint8ClampedArray(800); 54 | const d = new ImageData(data, 200); 55 | expect(d).toBeInstanceOf(ImageData); 56 | expect(d.height).toBe(1); 57 | expect(d.data).toBe(data); 58 | }); 59 | 60 | it('should throw if width is not finite', () => { 61 | expect(() => new ImageData(new Uint8ClampedArray(4), Infinity)).toThrow( 62 | RangeError 63 | ); 64 | }); 65 | 66 | it('should throw if width is 0', () => { 67 | expect(() => new ImageData(new Uint8ClampedArray(4), 0)).toThrow( 68 | RangeError 69 | ); 70 | }); 71 | 72 | it('should throw if source length is 0', () => { 73 | expect(() => new ImageData(new Uint8ClampedArray(0), 100)).toThrow( 74 | RangeError 75 | ); 76 | }); 77 | 78 | it('should throw if source length is not a multiple of 4', () => { 79 | expect(() => new ImageData(new Uint8ClampedArray(801), 200)).toThrow( 80 | RangeError 81 | ); 82 | }); 83 | }); 84 | 85 | describe('new ImageData(array, width, height)', () => { 86 | it('should construct an ImageData object', () => { 87 | const d = new ImageData(new Uint8ClampedArray([0, 0, 0, 1]), 1, 1); 88 | expect(d).toBeInstanceOf(ImageData); 89 | }); 90 | 91 | it('should set the width and height', () => { 92 | const d = new ImageData(new Uint8ClampedArray(80000), 100, 200); 93 | expect(d.width).toBe(100); 94 | expect(d.height).toBe(200); 95 | }); 96 | 97 | it('should throw if width is not finite', () => { 98 | expect( 99 | () => new ImageData(new Uint8ClampedArray(4), Infinity, 100) 100 | ).toThrow(RangeError); 101 | }); 102 | 103 | it('should throw if width is 0', () => { 104 | expect(() => new ImageData(new Uint8ClampedArray(4), 0, 100)).toThrow( 105 | RangeError 106 | ); 107 | }); 108 | 109 | it('should throw if height is not finite', () => { 110 | expect( 111 | () => new ImageData(new Uint8ClampedArray(4), 1, Infinity) 112 | ).toThrow(RangeError); 113 | }); 114 | 115 | it('should throw if height is 0', () => { 116 | expect(() => new ImageData(new Uint8ClampedArray(4), 100, 0)).toThrow( 117 | RangeError 118 | ); 119 | }); 120 | 121 | it('should throw if first argument is not a Uint8ClampedArray', () => { 122 | expect(() => new ImageData(0, 0, 100)).toThrow(TypeError); 123 | }); 124 | 125 | it('should throw if source length is 0', () => { 126 | expect(() => new ImageData(new Uint8ClampedArray(0), 100, 100)).toThrow( 127 | RangeError 128 | ); 129 | }); 130 | 131 | it('should throw if source length is not a multiple of 4', () => { 132 | expect(() => new ImageData(new Uint8ClampedArray(801), 200, 1)).toThrow( 133 | RangeError 134 | ); 135 | }); 136 | 137 | it("should throw if width and height aren't compatible with source length ", () => { 138 | expect(() => new ImageData(new Uint8ClampedArray(8), 2, 2)).toThrow( 139 | RangeError 140 | ); 141 | expect(() => new ImageData(new Uint8ClampedArray(8), 1, 7)).toThrow( 142 | RangeError 143 | ); 144 | expect(() => new ImageData(new Uint8ClampedArray(8), 7, 1)).toThrow( 145 | RangeError 146 | ); 147 | }); 148 | }); 149 | }); 150 | -------------------------------------------------------------------------------- /__tests__/classes/__snapshots__/CanvasRenderingContext2D.__getClippingPath.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`__getClippingRegion should be empty when there are no path elements 1`] = `[]`; 4 | 5 | exports[`__getClippingRegion should delete current clipping region when restored 1`] = ` 6 | [ 7 | { 8 | "props": { 9 | "height": 4, 10 | "width": 3, 11 | "x": 1, 12 | "y": 2, 13 | }, 14 | "transform": [ 15 | 1, 16 | 0, 17 | 0, 18 | 1, 19 | 0, 20 | 0, 21 | ], 22 | "type": "rect", 23 | }, 24 | ] 25 | `; 26 | 27 | exports[`__getClippingRegion should save the clipping region correctly when saved 1`] = ` 28 | [ 29 | { 30 | "props": { 31 | "height": 4, 32 | "width": 3, 33 | "x": 1, 34 | "y": 2, 35 | }, 36 | "transform": [ 37 | 1, 38 | 0, 39 | 0, 40 | 1, 41 | 0, 42 | 0, 43 | ], 44 | "type": "rect", 45 | }, 46 | { 47 | "props": { 48 | "anticlockwise": false, 49 | "endAngle": 5, 50 | "radius": 3, 51 | "startAngle": 4, 52 | "x": 1, 53 | "y": 2, 54 | }, 55 | "transform": [ 56 | 1, 57 | 0, 58 | 0, 59 | 1, 60 | 0, 61 | 0, 62 | ], 63 | "type": "arc", 64 | }, 65 | ] 66 | `; 67 | 68 | exports[`__getClippingRegion should save the clipping region correctly when saved 2`] = ` 69 | [ 70 | { 71 | "props": { 72 | "height": 4, 73 | "width": 3, 74 | "x": 1, 75 | "y": 2, 76 | }, 77 | "transform": [ 78 | 1, 79 | 0, 80 | 0, 81 | 1, 82 | 0, 83 | 0, 84 | ], 85 | "type": "rect", 86 | }, 87 | { 88 | "props": { 89 | "anticlockwise": false, 90 | "endAngle": 5, 91 | "radius": 3, 92 | "startAngle": 4, 93 | "x": 1, 94 | "y": 2, 95 | }, 96 | "transform": [ 97 | 1, 98 | 0, 99 | 0, 100 | 1, 101 | 0, 102 | 0, 103 | ], 104 | "type": "arc", 105 | }, 106 | ] 107 | `; 108 | 109 | exports[`__getClippingRegion should store the clipping region 1`] = ` 110 | [ 111 | { 112 | "props": { 113 | "height": 4, 114 | "width": 3, 115 | "x": 1, 116 | "y": 2, 117 | }, 118 | "transform": [ 119 | 1, 120 | 0, 121 | 0, 122 | 1, 123 | 0, 124 | 0, 125 | ], 126 | "type": "rect", 127 | }, 128 | { 129 | "props": { 130 | "anticlockwise": false, 131 | "endAngle": 5, 132 | "radius": 3, 133 | "startAngle": 4, 134 | "x": 1, 135 | "y": 2, 136 | }, 137 | "transform": [ 138 | 1, 139 | 0, 140 | 0, 141 | 1, 142 | 0, 143 | 0, 144 | ], 145 | "type": "arc", 146 | }, 147 | ] 148 | `; 149 | 150 | exports[`__getClippingRegion shouldn't store the whole clipping region twice when clip is called twice 1`] = ` 151 | [ 152 | { 153 | "props": { 154 | "height": 4, 155 | "width": 3, 156 | "x": 1, 157 | "y": 2, 158 | }, 159 | "transform": [ 160 | 1, 161 | 0, 162 | 0, 163 | 1, 164 | 0, 165 | 0, 166 | ], 167 | "type": "rect", 168 | }, 169 | { 170 | "props": { 171 | "anticlockwise": false, 172 | "endAngle": 5, 173 | "radius": 3, 174 | "startAngle": 4, 175 | "x": 1, 176 | "y": 2, 177 | }, 178 | "transform": [ 179 | 1, 180 | 0, 181 | 0, 182 | 1, 183 | 0, 184 | 0, 185 | ], 186 | "type": "arc", 187 | }, 188 | { 189 | "props": { 190 | "fillRule": "nonzero", 191 | "path": [ 192 | { 193 | "props": {}, 194 | "transform": [ 195 | 1, 196 | 0, 197 | 0, 198 | 1, 199 | 0, 200 | 0, 201 | ], 202 | "type": "beginPath", 203 | }, 204 | { 205 | "props": { 206 | "height": 4, 207 | "width": 3, 208 | "x": 1, 209 | "y": 2, 210 | }, 211 | "transform": [ 212 | 1, 213 | 0, 214 | 0, 215 | 1, 216 | 0, 217 | 0, 218 | ], 219 | "type": "rect", 220 | }, 221 | { 222 | "props": { 223 | "anticlockwise": false, 224 | "endAngle": 5, 225 | "radius": 3, 226 | "startAngle": 4, 227 | "x": 1, 228 | "y": 2, 229 | }, 230 | "transform": [ 231 | 1, 232 | 0, 233 | 0, 234 | 1, 235 | 0, 236 | 0, 237 | ], 238 | "type": "arc", 239 | }, 240 | ], 241 | }, 242 | "transform": [ 243 | 1, 244 | 0, 245 | 0, 246 | 1, 247 | 0, 248 | 0, 249 | ], 250 | "type": "clip", 251 | }, 252 | { 253 | "props": { 254 | "height": 4, 255 | "width": 3, 256 | "x": 1, 257 | "y": 2, 258 | }, 259 | "transform": [ 260 | 1, 261 | 0, 262 | 0, 263 | 1, 264 | 0, 265 | 0, 266 | ], 267 | "type": "rect", 268 | }, 269 | { 270 | "props": { 271 | "anticlockwise": false, 272 | "endAngle": 5, 273 | "radius": 3, 274 | "startAngle": 4, 275 | "x": 1, 276 | "y": 2, 277 | }, 278 | "transform": [ 279 | 1, 280 | 0, 281 | 0, 282 | 1, 283 | 0, 284 | 0, 285 | ], 286 | "type": "arc", 287 | }, 288 | ] 289 | `; 290 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jest-webgl-canvas-mock 2 | 3 | > Mock `canvas` and `WebGL` when run unit test cases with jest. For more browser environment, you can use [jest-electron](https://github.com/hustcc/jest-electron) for real browser runtime. 4 | 5 | [![Build Status](https://github.com/adamfsk/jest-webgl-canvas-mock/workflows/build/badge.svg)](https://github.com/adamfsk/jest-webgl-canvas-mock/actions/workflows/build.yml) 6 | [![npm](https://img.shields.io/npm/v/jest-webgl-canvas-mock.svg)](https://www.npmjs.com/package/jest-webgl-canvas-mock) 7 | [![npm](https://img.shields.io/npm/dm/jest-webgl-canvas-mock.svg)](https://www.npmjs.com/package/jest-webgl-canvas-mock) 8 | 9 | ## Disclaimer 10 | 11 | This project is a simple merge of [jest-canvas-mock](https://github.com/hustcc/jest-canvas-mock) with 12 | [webgl-mock](https://github.com/kbirk/webgl-mock) so that both 2d and webgl contexts can be tested in jest. 13 | As such, the only tests provided are those from the original projects. 14 | 15 | The current goal of this project is simply to make any tests using `pixi.js` work in jest. 16 | 17 | Please feel free to contribute and add any additional functionality required. 18 | 19 | ## Install 20 | 21 | This should only be installed as a development dependency (`devDependencies`) as it is only designed for testing. 22 | 23 | ```bash 24 | npm i --save-dev jest-webgl-canvas-mock 25 | ``` 26 | 27 | ## Setup 28 | 29 | In your `package.json` under the `jest`, create a `setupFiles` array and add `jest-webgl-canvas-mock` to the array. 30 | 31 | ```json 32 | { 33 | "jest": { 34 | "setupFiles": ["jest-webgl-canvas-mock"] 35 | } 36 | } 37 | ``` 38 | 39 | If you already have a `setupFiles` attribute you can also append `jest-webgl-canvas-mock` to the array. 40 | 41 | ```json 42 | { 43 | "jest": { 44 | "setupFiles": ["./__setups__/other.js", "jest-webgl-canvas-mock"] 45 | } 46 | } 47 | ``` 48 | 49 | More about in [configuration section](https://facebook.github.io/jest/docs/en/configuration.html#content). 50 | 51 | ## Setup file 52 | 53 | Alternatively you can create a new setup file which then requires this module or 54 | add the `require` statement to an existing setup file. 55 | 56 | `__setups__/canvas.js` 57 | 58 | ```js 59 | import 'jest-webgl-canvas-mock'; 60 | // or 61 | require('jest-webgl-canvas-mock'); 62 | ``` 63 | 64 | Add that file to your `setupFiles` array: 65 | 66 | ```json 67 | "jest": { 68 | "setupFiles": [ 69 | "./__setups__/canvas.js" 70 | ] 71 | } 72 | ``` 73 | 74 | ## Reset 75 | 76 | If you reset the jest mocks (for example, with `jest.resetAllMocks()`), you can 77 | call `setupJestCanvasMock()` to re-create it. 78 | 79 | ``` 80 | import { setupJestCanvasMock } from 'jest-webgl-canvas-mock'; 81 | 82 | beforeEach(() => { 83 | jest.resetAllMocks(); 84 | setupJestCanvasMock(); 85 | }); 86 | ``` 87 | 88 | ## Mock Strategy 89 | 90 | This mock strategy implements all the canvas functions and actually verifies the parameters. If a 91 | known condition would cause the browser to throw a `TypeError` or a `DOMException`, it emulates the 92 | error. For instance, the `CanvasRenderingContext2D#arc` function will throw a `TypeError` if the 93 | radius is negative, or if it was not provided with enough parameters. 94 | 95 | ```ts 96 | // arc throws a TypeError when the argument length is less than 5 97 | expect(() => ctx.arc(1, 2, 3, 4)).toThrow(TypeError); 98 | 99 | // when radius is negative, arc throws a dom exception when all parameters are finite 100 | expect(() => ctx.arc(0, 0, -10, 0, Math.PI * 2)).toThrow(DOMException); 101 | ``` 102 | 103 | The function will do `Number` type coercion and verify the inputs exactly like the browser does. So 104 | this is valid input. 105 | 106 | ```ts 107 | expect(() => ctx.arc('10', '10', '20', '0', '6.14')).not.toThrow(); 108 | ``` 109 | 110 | Another part of the strategy is to validate input types. When using the 111 | `CanvasRenderingContext2D#fill` function, if you pass it an invalid `fillRule` it will throw a 112 | `TypeError` just like the browser does. 113 | 114 | ```ts 115 | expect(() => ctx.fill('invalid!')).toThrow(TypeError); 116 | expect(() => ctx.fill(new Path2D(), 'invalid!')).toThrow(TypeError); 117 | ``` 118 | 119 | We try to follow the ECMAScript specification as closely as possible. 120 | 121 | ## Snapshots 122 | 123 | There are multiple ways to validate canvas state using snapshots. There are currently three methods 124 | attached to the `CanvasRenderingContext2D` class. The first way to use this feature is by using the 125 | `__getEvents` method. 126 | 127 | ```ts 128 | /** 129 | * In order to see which functions and properties were used for the test, you can use `__getEvents` 130 | * to gather this information. 131 | */ 132 | const events = ctx.__getEvents(); 133 | 134 | expect(events).toMatchSnapshot(); // jest will assert the events match the snapshot 135 | ``` 136 | 137 | The second way is to inspect the current path associated with the context. 138 | 139 | ```ts 140 | ctx.beginPath(); 141 | ctx.arc(1, 2, 3, 4, 5); 142 | ctx.moveTo(6, 7); 143 | ctx.rect(6, 7, 8, 9); 144 | ctx.closePath(); 145 | 146 | /** 147 | * Any method that modifies the current path (and subpath) will be pushed to an event array. When 148 | * using the `__getPath` method, that array will sliced and usable for snapshots. 149 | */ 150 | const path = ctx.__getPath(); 151 | expect(path).toMatchSnapshot(); 152 | ``` 153 | 154 | The third way is to inspect all of the successful draw calls submitted to the context. 155 | 156 | ```ts 157 | ctx.drawImage(img, 0, 0); 158 | 159 | /** 160 | * Every drawImage, fill, stroke, fillText, or strokeText function call will be logged in an event 161 | * array. This method will return those events here for inspection. 162 | */ 163 | const calls = ctx.__getDrawCalls(); 164 | expect(calls).toMatchSnapshot(); 165 | ``` 166 | 167 | In some cases it may be useful to clear the events or draw calls that have already been logged. 168 | 169 | ```ts 170 | // Clear events 171 | ctx.__clearEvents(); 172 | 173 | // Clear draw calls 174 | ctx.__clearDrawCalls(); 175 | ``` 176 | 177 | Finally, it's possible to inspect the clipping region calls by using the `__getClippingRegion` 178 | function. 179 | 180 | ```ts 181 | const clippingRegion = ctx.__getClippingRegion(); 182 | expect(clippingRegion).toMatchSnapshot(); 183 | ``` 184 | 185 | The clipping region cannot be cleared because it's based on the stack values and when the `.clip()` 186 | function is called. 187 | 188 | ## Override default mock return value 189 | 190 | You can override the default mock return value in your test to suit your need. For example, to override return value of `toDataURL`: 191 | 192 | ```ts 193 | canvas.toDataURL.mockReturnValueOnce( 194 | 'data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==' 195 | ); 196 | ``` 197 | 198 | ## Contributors 199 | 200 | - [@hustcc](https://github.com/hustcc) 201 | - [@jtenner](https://github.com/jtenner) 202 | - [@evanoc0](https://github.com/evanoc0) 203 | - [@lekha](https://github.com/lekha) 204 | - [@yonatankra](https://github.com/yonatankra) 205 | - [@LitoMore](https://github.com/LitoMore) 206 | 207 | ## License 208 | 209 | MIT 210 | -------------------------------------------------------------------------------- /src/classes/DOMMatrix.js: -------------------------------------------------------------------------------- 1 | function sumMultipleOfMatricesCells(matrix1Array, matrix2Array, { i, j }) { 2 | let sum = 0; 3 | for (let k = 0; k < 4; k++) { 4 | const matrix1Index = j - 1 + k * 4; 5 | const matrix2Index = (i - 1) * 4 + k; 6 | sum += matrix1Array[matrix1Index] * matrix2Array[matrix2Index]; 7 | } 8 | return sum; 9 | } 10 | 11 | function multiplyMatrices(leftMatrix, rightMatrix) { 12 | const leftMatrixArray = leftMatrix.toFloat64Array(); 13 | const rightMatrixArray = rightMatrix.toFloat64Array(); 14 | for (let i = 1; i <= 4; i++) { 15 | for (let j = 1; j <= 4; j++) { 16 | leftMatrix[`m${i}${j}`] = sumMultipleOfMatricesCells( 17 | leftMatrixArray, 18 | rightMatrixArray, 19 | { i, j } 20 | ); 21 | } 22 | } 23 | } 24 | 25 | export default class DOMMatrix { 26 | _is2D = true; 27 | m11 = 1.0; 28 | m12 = 0.0; 29 | m13 = 0.0; 30 | m14 = 0.0; 31 | m21 = 0.0; 32 | m22 = 1.0; 33 | m23 = 0.0; 34 | m24 = 0.0; 35 | m31 = 0.0; 36 | m32 = 0.0; 37 | m33 = 1.0; 38 | m34 = 0.0; 39 | m41 = 0.0; 40 | m42 = 0.0; 41 | m43 = 0.0; 42 | m44 = 1.0; 43 | 44 | constructor(transform) { 45 | if (transform && transform.length === 6) { 46 | this.m11 = transform[0]; 47 | this.m12 = transform[1]; 48 | this.m21 = transform[2]; 49 | this.m22 = transform[3]; 50 | this.m41 = transform[4]; 51 | this.m42 = transform[5]; 52 | this._is2D = true; 53 | return this; 54 | } 55 | 56 | if (transform && transform.length === 16) { 57 | this.m11 = transform[0]; 58 | this.m12 = transform[1]; 59 | this.m13 = transform[2]; 60 | this.m14 = transform[3]; 61 | this.m21 = transform[4]; 62 | this.m22 = transform[5]; 63 | this.m23 = transform[6]; 64 | this.m24 = transform[7]; 65 | this.m31 = transform[8]; 66 | this.m32 = transform[9]; 67 | this.m33 = transform[10]; 68 | this.m34 = transform[11]; 69 | this.m41 = transform[12]; 70 | this.m42 = transform[13]; 71 | this.m43 = transform[14]; 72 | this.m44 = transform[15]; 73 | this._is2D = false; 74 | return this; 75 | } 76 | 77 | if (transform) { 78 | throw new TypeError( 79 | "Failed to construct 'DOMMatrix': The sequence must contain 6 elements for a 2D matrix or 16 elements for a 3D matrix." 80 | ); 81 | } 82 | this._is2D = false; 83 | } 84 | 85 | get isIdentity() { 86 | if (this._is2D) { 87 | return ( 88 | this.m11 == 1.0 && 89 | this.m12 == 0.0 && 90 | this.m21 == 0.0 && 91 | this.m22 == 1.0 && 92 | this.m41 == 0.0 && 93 | this.m42 == 0.0 94 | ); 95 | } else { 96 | return (this.m11 = 97 | 1.0 && 98 | this.m12 === 0.0 && 99 | this.m13 === 0.0 && 100 | this.m14 === 0.0 && 101 | this.m21 === 0.0 && 102 | this.m22 === 1.0 && 103 | this.m23 === 0.0 && 104 | this.m24 === 0.0 && 105 | this.m31 === 0.0 && 106 | this.m32 === 0.0 && 107 | this.m33 === 1.0 && 108 | this.m34 === 0.0 && 109 | this.m41 === 0.0 && 110 | this.m42 === 0.0 && 111 | this.m43 === 0.0 && 112 | this.m44 === 1.0); 113 | } 114 | } 115 | 116 | get a() { 117 | return this.m11; 118 | } 119 | 120 | set a(value) { 121 | this.m11 = value; 122 | } 123 | 124 | get b() { 125 | return this.m12; 126 | } 127 | 128 | set b(value) { 129 | this.m12 = value; 130 | } 131 | 132 | get c() { 133 | return this.m21; 134 | } 135 | 136 | set c(value) { 137 | this.m21 = value; 138 | } 139 | 140 | get d() { 141 | return this.m22; 142 | } 143 | 144 | set d(value) { 145 | this.m22 = value; 146 | } 147 | 148 | get e() { 149 | return this.m41; 150 | } 151 | 152 | set e(value) { 153 | this.m41 = value; 154 | } 155 | 156 | get f() { 157 | return this.m42; 158 | } 159 | 160 | set f(value) { 161 | this.m42 = value; 162 | } 163 | 164 | get is2D() { 165 | return this._is2D; 166 | } 167 | 168 | toFloat32Array() { 169 | return new Float32Array([ 170 | this.m11, 171 | this.m12, 172 | this.m13, 173 | this.m14, 174 | this.m21, 175 | this.m22, 176 | this.m23, 177 | this.m24, 178 | this.m31, 179 | this.m32, 180 | this.m33, 181 | this.m34, 182 | this.m41, 183 | this.m42, 184 | this.m43, 185 | this.m44, 186 | ]); 187 | } 188 | 189 | toFloat64Array() { 190 | return new Float64Array([ 191 | this.m11, 192 | this.m12, 193 | this.m13, 194 | this.m14, 195 | this.m21, 196 | this.m22, 197 | this.m23, 198 | this.m24, 199 | this.m31, 200 | this.m32, 201 | this.m33, 202 | this.m34, 203 | this.m41, 204 | this.m42, 205 | this.m43, 206 | this.m44, 207 | ]); 208 | } 209 | 210 | translateSelf(x, y, z) { 211 | const tx = Number(x), 212 | ty = Number(y), 213 | tz = isNaN(Number(z)) ? 0 : Number(z); 214 | 215 | const translationMatrix = new DOMMatrix(); 216 | translationMatrix.m41 = tx; 217 | translationMatrix.m42 = ty; 218 | translationMatrix.m43 = tz; 219 | 220 | multiplyMatrices(this, translationMatrix); 221 | 222 | if (tz) { 223 | this._is2D = false; 224 | } 225 | return this; 226 | } 227 | 228 | translate(x, y, z) { 229 | let translatedMatrix; 230 | if (this.is2D) { 231 | translatedMatrix = new DOMMatrix([ 232 | this.a, 233 | this.b, 234 | this.c, 235 | this.d, 236 | this.e, 237 | this.f, 238 | ]); 239 | } else { 240 | translatedMatrix = new DOMMatrix(this.toFloat32Array()); 241 | } 242 | 243 | return translatedMatrix.translateSelf(x, y, z); 244 | } 245 | 246 | scaleSelf(scaleX, scaleY, scaleZ, originX, originY, originZ) { 247 | const sx = Number(scaleX), 248 | sy = isNaN(Number(scaleY)) ? sx : Number(scaleY), 249 | sz = isNaN(Number(scaleZ)) ? 1 : Number(scaleZ); 250 | 251 | const ox = isNaN(Number(originX)) ? 0 : Number(originX), 252 | oy = isNaN(Number(originY)) ? 0 : Number(originY), 253 | oz = isNaN(Number(originZ)) ? 0 : Number(originZ); 254 | 255 | this.translateSelf(ox, oy, oz); 256 | 257 | const scaleMatrix = new DOMMatrix(); 258 | scaleMatrix.m11 = sx; 259 | scaleMatrix.m22 = sy; 260 | scaleMatrix.m33 = sz; 261 | 262 | multiplyMatrices(this, scaleMatrix); 263 | 264 | this.translateSelf(-ox, -oy, -oz); 265 | 266 | if (Math.abs(sz) !== 1) { 267 | this._is2D = false; 268 | } 269 | return this; 270 | } 271 | 272 | scale(scaleX, scaleY, scaleZ, originX, originY, originZ) { 273 | let scaledMatrix; 274 | if (this.is2D) { 275 | scaledMatrix = new DOMMatrix([ 276 | this.a, 277 | this.b, 278 | this.c, 279 | this.d, 280 | this.e, 281 | this.f, 282 | ]); 283 | } else { 284 | scaledMatrix = new DOMMatrix(this.toFloat32Array()); 285 | } 286 | 287 | return scaledMatrix.scaleSelf( 288 | scaleX, 289 | scaleY, 290 | scaleZ, 291 | originX, 292 | originY, 293 | originZ 294 | ); 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /__tests__/classes/DOMMatrix.js: -------------------------------------------------------------------------------- 1 | import DOMMatrix from '../../src/classes/DOMMatrix'; 2 | 3 | describe('DOMMatrix class', () => { 4 | it('should accept no parameters', () => { 5 | const matrix = new DOMMatrix(); 6 | expect(matrix).toBeInstanceOf(DOMMatrix); 7 | }); 8 | 9 | it('should construct a 2d matrix properly', () => { 10 | const matrix = new DOMMatrix([1, 2, 3, 4, 5, 6]); 11 | expect(matrix.a).toBe(1); 12 | expect(matrix.b).toBe(2); 13 | expect(matrix.c).toBe(3); 14 | expect(matrix.d).toBe(4); 15 | expect(matrix.e).toBe(5); 16 | expect(matrix.f).toBe(6); 17 | }); 18 | 19 | it('should be a 3d matrix if constructed without a parameter', () => { 20 | const matrix = new DOMMatrix(); 21 | expect(matrix.is2D).toBeFalsy(); 22 | }); 23 | 24 | it('should throw for invalid parameter length', () => { 25 | expect(() => new DOMMatrix([1])).toThrow(); 26 | }); 27 | 28 | it('should accept an array of 6 length', () => { 29 | const matrix = new DOMMatrix([1, 0, 0, 1, 0, 0]); 30 | expect(matrix).toBeInstanceOf(DOMMatrix); 31 | }); 32 | 33 | it('should accept an array of 16 length', () => { 34 | const matrix = new DOMMatrix([ 35 | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 36 | ]); 37 | expect(matrix).toBeInstanceOf(DOMMatrix); 38 | }); 39 | 40 | it('should return Float32Array', () => { 41 | const matrix32 = new DOMMatrix().toFloat32Array(); 42 | expect(matrix32).toBeInstanceOf(Float32Array); 43 | expect(matrix32).toStrictEqual( 44 | new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]) 45 | ); 46 | }); 47 | 48 | it('should return Float64Array', () => { 49 | const matrix64 = new DOMMatrix().toFloat64Array(); 50 | expect(matrix64).toBeInstanceOf(Float64Array); 51 | expect(matrix64).toStrictEqual( 52 | new Float64Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]) 53 | ); 54 | }); 55 | 56 | it('should know if a 2d matrix is an identity matrix', () => { 57 | const matrix = new DOMMatrix([1, 0, 0, 1, 0, 0]); 58 | expect(matrix.isIdentity).toBeTruthy(); 59 | }); 60 | 61 | it('should know if a 2d matrix is not an identity matrix', () => { 62 | const matrix = new DOMMatrix([1, 2, 3, 4, 5, 6]); 63 | expect(matrix.isIdentity).toBeFalsy(); 64 | }); 65 | 66 | it('should know if a 3d matrix is an identity matrix', () => { 67 | const matrix = new DOMMatrix(); 68 | expect(matrix.isIdentity).toBeTruthy(); 69 | }); 70 | 71 | it('should know if a 3d matrix is not an identity matrix', () => { 72 | const matrix = new DOMMatrix(); 73 | matrix.m21 = 100; 74 | expect(matrix.isIdentity).toBeFalsy(); 75 | }); 76 | 77 | it('should set the m11 value when the a value is set', () => { 78 | const matrix = new DOMMatrix([1, 0, 0, 1, 0, 0]); 79 | matrix.a = 2; 80 | expect(matrix.m11).toBe(2); 81 | }); 82 | 83 | it('should return the m11 value when the a property is accessed', () => { 84 | const matrix = new DOMMatrix([1, 0, 0, 1, 0, 0]); 85 | matrix.m11 = 2; 86 | expect(matrix.a).toBe(2); 87 | }); 88 | 89 | it('should set the m12 value when the b value is set', () => { 90 | const matrix = new DOMMatrix([1, 0, 0, 1, 0, 0]); 91 | matrix.b = 2; 92 | expect(matrix.m12).toBe(2); 93 | }); 94 | 95 | it('should return the m12 value when the b property is accessed', () => { 96 | const matrix = new DOMMatrix([1, 0, 0, 1, 0, 0]); 97 | matrix.m12 = 2; 98 | expect(matrix.b).toBe(2); 99 | }); 100 | 101 | it('should set the m21 value when the c value is set', () => { 102 | const matrix = new DOMMatrix([1, 0, 0, 1, 0, 0]); 103 | matrix.c = 2; 104 | expect(matrix.m21).toBe(2); 105 | }); 106 | 107 | it('should return the m21 value when the c property is accessed', () => { 108 | const matrix = new DOMMatrix([1, 0, 0, 1, 0, 0]); 109 | matrix.m21 = 2; 110 | expect(matrix.c).toBe(2); 111 | }); 112 | 113 | it('should set the m22 value when the d value is set', () => { 114 | const matrix = new DOMMatrix([1, 0, 0, 1, 0, 0]); 115 | matrix.d = 2; 116 | expect(matrix.m22).toBe(2); 117 | }); 118 | 119 | it('should return the m22 value when the d property is accessed', () => { 120 | const matrix = new DOMMatrix([1, 0, 0, 1, 0, 0]); 121 | matrix.m22 = 2; 122 | expect(matrix.d).toBe(2); 123 | }); 124 | 125 | it('should set the m41 value when the e value is set', () => { 126 | const matrix = new DOMMatrix([1, 0, 0, 1, 0, 0]); 127 | matrix.e = 2; 128 | expect(matrix.m41).toBe(2); 129 | }); 130 | 131 | it('should return the m41 value when the e property is accessed', () => { 132 | const matrix = new DOMMatrix([1, 0, 0, 1, 0, 0]); 133 | matrix.m41 = 2; 134 | expect(matrix.e).toBe(2); 135 | }); 136 | 137 | it('should set the m42 value when the f value is set', () => { 138 | const matrix = new DOMMatrix([1, 0, 0, 1, 0, 0]); 139 | matrix.f = 2; 140 | expect(matrix.m42).toBe(2); 141 | }); 142 | 143 | it('should return the m42 value when the f property is accessed', () => { 144 | const matrix = new DOMMatrix([1, 0, 0, 1, 0, 0]); 145 | matrix.m42 = 2; 146 | expect(matrix.f).toBe(2); 147 | }); 148 | 149 | describe(`translate`, function () { 150 | it(`should return a new DOMMatrix instance`, function () { 151 | const matrix = new DOMMatrix(); 152 | const translatedMatrix = matrix.translate(100, 100); 153 | expect(translatedMatrix instanceof DOMMatrix).toBeTruthy(); 154 | expect(translatedMatrix === matrix).toBeFalsy(); 155 | }); 156 | 157 | it(`should apply 2d changes`, function () { 158 | const x = 100; 159 | const y = 200; 160 | const matrix = new DOMMatrix([4, 5, 1, 3, 10, 9]); 161 | const expectedMatrix = new DOMMatrix([ 162 | 4, 5, 0, 0, 1, 3, 0, 0, 0, 0, 1, 0, 610, 1109, 0, 1, 163 | ]); 164 | const translatedMatrix = matrix.translate(x, y); 165 | expect(translatedMatrix.toFloat32Array()).toEqual( 166 | expectedMatrix.toFloat32Array() 167 | ); 168 | expect(translatedMatrix.is2D).toEqual(true); 169 | }); 170 | 171 | it(`should apply 3d changes`, function () { 172 | const x = 100; 173 | const y = 200; 174 | const z = 300; 175 | const matrix = new DOMMatrix(); 176 | const expectedMatrix = new DOMMatrix([ 177 | 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 100, 200, 300, 1, 178 | ]); 179 | const translatedMatrix = matrix.translate(x, y, z); 180 | expect(translatedMatrix).toEqual(expectedMatrix); 181 | expect(translatedMatrix.is2D).toEqual(false); 182 | }); 183 | }); 184 | 185 | describe(`scale`, function () { 186 | it(`should return a new DOMMatrix instance`, function () { 187 | const matrix = new DOMMatrix(); 188 | const scaledMatrix = matrix.scale(0.5, 0.7); 189 | expect(scaledMatrix instanceof DOMMatrix).toBeTruthy(); 190 | expect(scaledMatrix === matrix).toBeFalsy(); 191 | }); 192 | 193 | it(`should apply 2d changes`, function () { 194 | const scaleX = 0.75; 195 | const scaleY = 0.5; 196 | const matrix = new DOMMatrix([7, 8, 9, 20, 4, 7]); 197 | const expectedMatrix = new DOMMatrix([5.25, 6, 4.5, 10, 4, 7]); 198 | const scaledMatrix = matrix.scale(scaleX, scaleY); 199 | expect(scaledMatrix).toEqual(expectedMatrix); 200 | }); 201 | 202 | it(`should apply 3d changes`, function () { 203 | const scaleX = 0.65; 204 | const scaleY = 0.55; 205 | const scaleZ = 0.9; 206 | const matrix = new DOMMatrix(); 207 | const expectedMatrix = new DOMMatrix([ 208 | 0.65, 0, 0, 0, 0, 0.55, 0, 0, 0, 0, 0.9, 0, 0, 0, 0, 1, 209 | ]); 210 | const scaledMatrix = matrix.scale(scaleX, scaleY, scaleZ); 211 | expect(scaledMatrix).toEqual(expectedMatrix); 212 | }); 213 | }); 214 | 215 | describe(`translateSelf`, function () { 216 | it(`should return dot product of a 2d matrix multiplication`, function () { 217 | const matrix2D = new DOMMatrix([1, 2, 3, 4, 5, 6]); 218 | const tx = 2, 219 | ty = 3; 220 | const expectedMatrix = new DOMMatrix([1, 2, 3, 4, 16, 22]); 221 | matrix2D.translateSelf(tx, ty); 222 | expect(matrix2D.toFloat32Array()).toEqual( 223 | expectedMatrix.toFloat32Array() 224 | ); 225 | expect(matrix2D.is2D).toEqual(true); 226 | }); 227 | 228 | it(`should return do product of a 3d matrix`, function () { 229 | const matrix3D = new DOMMatrix([ 230 | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 231 | ]); 232 | const tx = 2, 233 | ty = 3, 234 | tz = 4; 235 | const expectedMatrix = new DOMMatrix([ 236 | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 66, 76, 86, 96, 237 | ]); 238 | matrix3D.translateSelf(tx, ty, tz); 239 | expect(matrix3D.toFloat32Array()).toEqual( 240 | expectedMatrix.toFloat32Array() 241 | ); 242 | expect(matrix3D.is2D).toEqual(false); 243 | }); 244 | 245 | it(`should convert 2d matrix to 3d matrix when sent tz`, function () { 246 | const matrix2D = new DOMMatrix([1, 2, 3, 4, 5, 6]); 247 | const tx = 2, 248 | ty = 3, 249 | tz = 4; 250 | const expectedMatrix = new DOMMatrix([ 251 | 1, 2, 0, 0, 3, 4, 0, 0, 0, 0, 1, 0, 16, 22, 4, 1, 252 | ]); 253 | matrix2D.translateSelf(tx, ty, tz); 254 | expect(matrix2D.toFloat32Array()).toEqual( 255 | expectedMatrix.toFloat32Array() 256 | ); 257 | expect(matrix2D.is2D).toEqual(false); 258 | }); 259 | }); 260 | 261 | describe(`scaleSelf`, function () { 262 | it(`should return dot product of a 2d translated matrix multiplication`, function () { 263 | const matrix2D = new DOMMatrix([1, 2, 3, 4, 5, 6]); 264 | const scaleX = 2, 265 | scaleY = 3; 266 | const expectedMatrix = new DOMMatrix([2, 4, 9, 12, 5, 6]); 267 | matrix2D.scaleSelf(scaleX, scaleY); 268 | expect(matrix2D).toEqual(expectedMatrix); 269 | }); 270 | 271 | it(`should return dot product of a 3d translated matrix multiplication`, function () { 272 | const matrix3D = new DOMMatrix([ 273 | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 274 | ]); 275 | const sx = 2, 276 | sy = 3, 277 | sz = 4; 278 | const expectedMatrix = new DOMMatrix([ 279 | 2, 4, 6, 8, 15, 18, 21, 24, 36, 40, 44, 48, 13, 14, 15, 16, 280 | ]); 281 | matrix3D.scaleSelf(sx, sy, sz); 282 | expect(matrix3D).toEqual(expectedMatrix); 283 | }); 284 | 285 | it(`should return dot product of a 3d translated matrix multiplication with origin`, function () { 286 | const matrix3D = new DOMMatrix([ 287 | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 288 | ]); 289 | const sx = 2, 290 | sy = 3, 291 | sz = 4, 292 | ox = 5, 293 | oy = 6, 294 | oz = 7; 295 | const expectedMatrix = new DOMMatrix([ 296 | 2, 4, 6, 8, 15, 18, 21, 24, 36, 40, 44, 48, -241, -278, -315, -352, 297 | ]); 298 | matrix3D.scaleSelf(sx, sy, sz, ox, oy, oz); 299 | expect(matrix3D).toEqual(expectedMatrix); 300 | }); 301 | }); 302 | }); 303 | -------------------------------------------------------------------------------- /__tests__/classes/__snapshots__/CanvasRenderingContext2D.__getDrawCalls.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`__getDrawCalls should have a draw call when clearRect is called 1`] = ` 4 | [ 5 | { 6 | "props": { 7 | "height": 4, 8 | "width": 3, 9 | "x": 1, 10 | "y": 2, 11 | }, 12 | "transform": [ 13 | 1, 14 | 0, 15 | 0, 16 | 1, 17 | 0, 18 | 0, 19 | ], 20 | "type": "clearRect", 21 | }, 22 | ] 23 | `; 24 | 25 | exports[`__getDrawCalls should have a draw call when drawImage is called 1`] = ` 26 | [ 27 | { 28 | "props": { 29 | "dHeight": 300, 30 | "dWidth": 400, 31 | "dx": 0, 32 | "dy": 0, 33 | "img": , 38 | "sHeight": 300, 39 | "sWidth": 400, 40 | "sx": 0, 41 | "sy": 0, 42 | }, 43 | "transform": [ 44 | 1, 45 | 0, 46 | 0, 47 | 1, 48 | 0, 49 | 0, 50 | ], 51 | "type": "drawImage", 52 | }, 53 | ] 54 | `; 55 | 56 | exports[`__getDrawCalls should have a draw call when drawImage is called with size parameters 1`] = ` 57 | [ 58 | { 59 | "props": { 60 | "dHeight": 300, 61 | "dWidth": 400, 62 | "dx": 0, 63 | "dy": 0, 64 | "img": , 69 | "sHeight": 300, 70 | "sWidth": 400, 71 | "sx": 0, 72 | "sy": 0, 73 | }, 74 | "transform": [ 75 | 1, 76 | 0, 77 | 0, 78 | 1, 79 | 0, 80 | 0, 81 | ], 82 | "type": "drawImage", 83 | }, 84 | ] 85 | `; 86 | 87 | exports[`__getDrawCalls should have a draw call when drawImage is called with source parameters 1`] = ` 88 | [ 89 | { 90 | "props": { 91 | "dHeight": 200, 92 | "dWidth": 200, 93 | "dx": 0, 94 | "dy": 0, 95 | "img": , 100 | "sHeight": 100, 101 | "sWidth": 100, 102 | "sx": 0, 103 | "sy": 0, 104 | }, 105 | "transform": [ 106 | 1, 107 | 0, 108 | 0, 109 | 1, 110 | 0, 111 | 0, 112 | ], 113 | "type": "drawImage", 114 | }, 115 | ] 116 | `; 117 | 118 | exports[`__getDrawCalls should have a draw call when fill() is called 1`] = ` 119 | [ 120 | { 121 | "props": { 122 | "fillRule": "nonzero", 123 | "path": [ 124 | { 125 | "props": {}, 126 | "transform": [ 127 | 1, 128 | 0, 129 | 0, 130 | 1, 131 | 0, 132 | 0, 133 | ], 134 | "type": "beginPath", 135 | }, 136 | { 137 | "props": { 138 | "anticlockwise": false, 139 | "endAngle": 6.283185307179586, 140 | "radius": 10, 141 | "startAngle": 0, 142 | "x": 100, 143 | "y": 101, 144 | }, 145 | "transform": [ 146 | 1, 147 | 0, 148 | 0, 149 | 1, 150 | 0, 151 | 0, 152 | ], 153 | "type": "arc", 154 | }, 155 | ], 156 | }, 157 | "transform": [ 158 | 1, 159 | 0, 160 | 0, 161 | 1, 162 | 0, 163 | 0, 164 | ], 165 | "type": "fill", 166 | }, 167 | ] 168 | `; 169 | 170 | exports[`__getDrawCalls should have a draw call when fill() is called with a fillRule 1`] = ` 171 | [ 172 | { 173 | "props": { 174 | "fillRule": "evenodd", 175 | "path": [ 176 | { 177 | "props": {}, 178 | "transform": [ 179 | 1, 180 | 0, 181 | 0, 182 | 1, 183 | 0, 184 | 0, 185 | ], 186 | "type": "beginPath", 187 | }, 188 | { 189 | "props": { 190 | "anticlockwise": false, 191 | "endAngle": 6.283185307179586, 192 | "radius": 10, 193 | "startAngle": 0, 194 | "x": 100, 195 | "y": 101, 196 | }, 197 | "transform": [ 198 | 1, 199 | 0, 200 | 0, 201 | 1, 202 | 0, 203 | 0, 204 | ], 205 | "type": "arc", 206 | }, 207 | ], 208 | }, 209 | "transform": [ 210 | 1, 211 | 0, 212 | 0, 213 | 1, 214 | 0, 215 | 0, 216 | ], 217 | "type": "fill", 218 | }, 219 | ] 220 | `; 221 | 222 | exports[`__getDrawCalls should have a draw call when fillRect is called 1`] = ` 223 | [ 224 | { 225 | "props": { 226 | "height": 4, 227 | "width": 3, 228 | "x": 1, 229 | "y": 2, 230 | }, 231 | "transform": [ 232 | 1, 233 | 0, 234 | 0, 235 | 1, 236 | 0, 237 | 0, 238 | ], 239 | "type": "fillRect", 240 | }, 241 | ] 242 | `; 243 | 244 | exports[`__getDrawCalls should have a draw call when fillRule is valid 1`] = ` 245 | [ 246 | { 247 | "props": { 248 | "fillRule": "evenodd", 249 | "path": [ 250 | { 251 | "props": { 252 | "anticlockwise": false, 253 | "endAngle": 6.283185307179586, 254 | "radius": 10, 255 | "startAngle": 0, 256 | "x": 100, 257 | "y": 101, 258 | }, 259 | "transform": [ 260 | 1, 261 | 0, 262 | 0, 263 | 1, 264 | 0, 265 | 0, 266 | ], 267 | "type": "arc", 268 | }, 269 | ], 270 | }, 271 | "transform": [ 272 | 1, 273 | 0, 274 | 0, 275 | 1, 276 | 0, 277 | 0, 278 | ], 279 | "type": "fill", 280 | }, 281 | ] 282 | `; 283 | 284 | exports[`__getDrawCalls should have a draw call when fillText has valid maxWidth 1`] = ` 285 | [ 286 | { 287 | "props": { 288 | "maxWidth": 100, 289 | "text": "hello world", 290 | "x": 0, 291 | "y": 0, 292 | }, 293 | "transform": [ 294 | 1, 295 | 0, 296 | 0, 297 | 1, 298 | 0, 299 | 0, 300 | ], 301 | "type": "fillText", 302 | }, 303 | ] 304 | `; 305 | 306 | exports[`__getDrawCalls should have a draw call when fillText is valid 1`] = ` 307 | [ 308 | { 309 | "props": { 310 | "maxWidth": null, 311 | "text": "hello world", 312 | "x": 0, 313 | "y": 0, 314 | }, 315 | "transform": [ 316 | 1, 317 | 0, 318 | 0, 319 | 1, 320 | 0, 321 | 0, 322 | ], 323 | "type": "fillText", 324 | }, 325 | ] 326 | `; 327 | 328 | exports[`__getDrawCalls should have a draw call when stroke is called 1`] = ` 329 | [ 330 | { 331 | "props": { 332 | "path": [ 333 | { 334 | "props": {}, 335 | "transform": [ 336 | 1, 337 | 0, 338 | 0, 339 | 1, 340 | 0, 341 | 0, 342 | ], 343 | "type": "beginPath", 344 | }, 345 | { 346 | "props": { 347 | "anticlockwise": false, 348 | "endAngle": 6.283185307179586, 349 | "radius": 10, 350 | "startAngle": 0, 351 | "x": 100, 352 | "y": 101, 353 | }, 354 | "transform": [ 355 | 1, 356 | 0, 357 | 0, 358 | 1, 359 | 0, 360 | 0, 361 | ], 362 | "type": "arc", 363 | }, 364 | ], 365 | }, 366 | "transform": [ 367 | 1, 368 | 0, 369 | 0, 370 | 1, 371 | 0, 372 | 0, 373 | ], 374 | "type": "stroke", 375 | }, 376 | ] 377 | `; 378 | 379 | exports[`__getDrawCalls should have a draw call when stroke is called with a path 1`] = ` 380 | [ 381 | { 382 | "props": { 383 | "path": [ 384 | { 385 | "props": { 386 | "anticlockwise": false, 387 | "endAngle": 6.283185307179586, 388 | "radius": 10, 389 | "startAngle": 0, 390 | "x": 100, 391 | "y": 101, 392 | }, 393 | "transform": [ 394 | 1, 395 | 0, 396 | 0, 397 | 1, 398 | 0, 399 | 0, 400 | ], 401 | "type": "arc", 402 | }, 403 | ], 404 | }, 405 | "transform": [ 406 | 1, 407 | 0, 408 | 0, 409 | 1, 410 | 0, 411 | 0, 412 | ], 413 | "type": "stroke", 414 | }, 415 | ] 416 | `; 417 | 418 | exports[`__getDrawCalls should have a draw call when strokeRect is called 1`] = ` 419 | [ 420 | { 421 | "props": { 422 | "height": 4, 423 | "width": 3, 424 | "x": 1, 425 | "y": 2, 426 | }, 427 | "transform": [ 428 | 1, 429 | 0, 430 | 0, 431 | 1, 432 | 0, 433 | 0, 434 | ], 435 | "type": "strokeRect", 436 | }, 437 | ] 438 | `; 439 | 440 | exports[`__getDrawCalls should have a draw call when strokeText has valid maxWidth 1`] = ` 441 | [ 442 | { 443 | "props": { 444 | "maxWidth": 100, 445 | "text": "hello world", 446 | "x": 0, 447 | "y": 0, 448 | }, 449 | "transform": [ 450 | 1, 451 | 0, 452 | 0, 453 | 1, 454 | 0, 455 | 0, 456 | ], 457 | "type": "strokeText", 458 | }, 459 | ] 460 | `; 461 | 462 | exports[`__getDrawCalls should have a draw call when strokeText is valid 1`] = ` 463 | [ 464 | { 465 | "props": { 466 | "maxWidth": null, 467 | "text": "hello world", 468 | "x": 0, 469 | "y": 0, 470 | }, 471 | "transform": [ 472 | 1, 473 | 0, 474 | 0, 475 | 1, 476 | 0, 477 | 0, 478 | ], 479 | "type": "strokeText", 480 | }, 481 | ] 482 | `; 483 | 484 | exports[`__getDrawCalls should have a draw call when using a path 1`] = ` 485 | [ 486 | { 487 | "props": { 488 | "fillRule": "nonzero", 489 | "path": [ 490 | { 491 | "props": { 492 | "anticlockwise": false, 493 | "endAngle": 6.283185307179586, 494 | "radius": 10, 495 | "startAngle": 0, 496 | "x": 100, 497 | "y": 101, 498 | }, 499 | "transform": [ 500 | 1, 501 | 0, 502 | 0, 503 | 1, 504 | 0, 505 | 0, 506 | ], 507 | "type": "arc", 508 | }, 509 | ], 510 | }, 511 | "transform": [ 512 | 1, 513 | 0, 514 | 0, 515 | 1, 516 | 0, 517 | 0, 518 | ], 519 | "type": "fill", 520 | }, 521 | ] 522 | `; 523 | 524 | exports[`__getDrawCalls should not have a draw call when clearRect is passed bad values 1`] = `[]`; 525 | 526 | exports[`__getDrawCalls should not have a draw call when drawImage is called with non-finite numbers 1`] = `[]`; 527 | 528 | exports[`__getDrawCalls should not have a draw call when drawImage is called with non-finite numbers and size parameters 1`] = `[]`; 529 | 530 | exports[`__getDrawCalls should not have a draw call when drawImage is called with non-finite numbers and source parameters 1`] = `[]`; 531 | 532 | exports[`__getDrawCalls should not have a draw call when fillRect is passed bad values 1`] = `[]`; 533 | 534 | exports[`__getDrawCalls should not have a draw call when fillText is not valid 1`] = `[]`; 535 | 536 | exports[`__getDrawCalls should not have a draw call when fillText is not valid 2`] = `[]`; 537 | 538 | exports[`__getDrawCalls should not have a draw call when strokeRect is passed bad values 1`] = `[]`; 539 | 540 | exports[`__getDrawCalls should not have a draw call when strokeText is not valid 1`] = `[]`; 541 | 542 | exports[`__getDrawCalls should not have a draw call when strokeText is not valid 2`] = `[]`; 543 | -------------------------------------------------------------------------------- /__tests__/classes/__snapshots__/CanvasRenderingContext2D.__getPath.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`__getPath should have a path item when arc is called 1`] = ` 4 | [ 5 | { 6 | "props": {}, 7 | "transform": [ 8 | 1, 9 | 0, 10 | 0, 11 | 1, 12 | 0, 13 | 0, 14 | ], 15 | "type": "beginPath", 16 | }, 17 | { 18 | "props": { 19 | "anticlockwise": true, 20 | "endAngle": 5, 21 | "radius": 3, 22 | "startAngle": 4, 23 | "x": 1, 24 | "y": 2, 25 | }, 26 | "transform": [ 27 | 1, 28 | 0, 29 | 0, 30 | 1, 31 | 0, 32 | 0, 33 | ], 34 | "type": "arc", 35 | }, 36 | ] 37 | `; 38 | 39 | exports[`__getPath should have a path item when arcTo is called 1`] = ` 40 | [ 41 | { 42 | "props": {}, 43 | "transform": [ 44 | 1, 45 | 0, 46 | 0, 47 | 1, 48 | 0, 49 | 0, 50 | ], 51 | "type": "beginPath", 52 | }, 53 | { 54 | "props": { 55 | "cpx1": 1, 56 | "cpx2": 3, 57 | "cpy1": 2, 58 | "cpy2": 4, 59 | "radius": 5, 60 | }, 61 | "transform": [ 62 | 1, 63 | 0, 64 | 0, 65 | 1, 66 | 0, 67 | 0, 68 | ], 69 | "type": "arcTo", 70 | }, 71 | ] 72 | `; 73 | 74 | exports[`__getPath should have a path item when bezierCurveTo is called 1`] = ` 75 | [ 76 | { 77 | "props": {}, 78 | "transform": [ 79 | 1, 80 | 0, 81 | 0, 82 | 1, 83 | 0, 84 | 0, 85 | ], 86 | "type": "beginPath", 87 | }, 88 | { 89 | "props": { 90 | "cpx1": 1, 91 | "cpx2": 3, 92 | "cpy1": 2, 93 | "cpy2": 4, 94 | "x": 5, 95 | "y": 6, 96 | }, 97 | "transform": [ 98 | 1, 99 | 0, 100 | 0, 101 | 1, 102 | 0, 103 | 0, 104 | ], 105 | "type": "bezierCurveTo", 106 | }, 107 | ] 108 | `; 109 | 110 | exports[`__getPath should have a path item when clip is called 1`] = ` 111 | [ 112 | { 113 | "props": {}, 114 | "transform": [ 115 | 1, 116 | 0, 117 | 0, 118 | 1, 119 | 0, 120 | 0, 121 | ], 122 | "type": "beginPath", 123 | }, 124 | { 125 | "props": { 126 | "height": 4, 127 | "width": 3, 128 | "x": 1, 129 | "y": 2, 130 | }, 131 | "transform": [ 132 | 1, 133 | 0, 134 | 0, 135 | 1, 136 | 0, 137 | 0, 138 | ], 139 | "type": "rect", 140 | }, 141 | { 142 | "props": { 143 | "fillRule": "nonzero", 144 | "path": [ 145 | { 146 | "props": {}, 147 | "transform": [ 148 | 1, 149 | 0, 150 | 0, 151 | 1, 152 | 0, 153 | 0, 154 | ], 155 | "type": "beginPath", 156 | }, 157 | { 158 | "props": { 159 | "height": 4, 160 | "width": 3, 161 | "x": 1, 162 | "y": 2, 163 | }, 164 | "transform": [ 165 | 1, 166 | 0, 167 | 0, 168 | 1, 169 | 0, 170 | 0, 171 | ], 172 | "type": "rect", 173 | }, 174 | ], 175 | }, 176 | "transform": [ 177 | 1, 178 | 0, 179 | 0, 180 | 1, 181 | 0, 182 | 0, 183 | ], 184 | "type": "clip", 185 | }, 186 | ] 187 | `; 188 | 189 | exports[`__getPath should have a path item when clip is called with a fillRule 1`] = ` 190 | [ 191 | { 192 | "props": {}, 193 | "transform": [ 194 | 1, 195 | 0, 196 | 0, 197 | 1, 198 | 0, 199 | 0, 200 | ], 201 | "type": "beginPath", 202 | }, 203 | { 204 | "props": { 205 | "height": 4, 206 | "width": 3, 207 | "x": 1, 208 | "y": 2, 209 | }, 210 | "transform": [ 211 | 1, 212 | 0, 213 | 0, 214 | 1, 215 | 0, 216 | 0, 217 | ], 218 | "type": "rect", 219 | }, 220 | { 221 | "props": { 222 | "fillRule": "evenodd", 223 | "path": [ 224 | { 225 | "props": {}, 226 | "transform": [ 227 | 1, 228 | 0, 229 | 0, 230 | 1, 231 | 0, 232 | 0, 233 | ], 234 | "type": "beginPath", 235 | }, 236 | { 237 | "props": { 238 | "height": 4, 239 | "width": 3, 240 | "x": 1, 241 | "y": 2, 242 | }, 243 | "transform": [ 244 | 1, 245 | 0, 246 | 0, 247 | 1, 248 | 0, 249 | 0, 250 | ], 251 | "type": "rect", 252 | }, 253 | ], 254 | }, 255 | "transform": [ 256 | 1, 257 | 0, 258 | 0, 259 | 1, 260 | 0, 261 | 0, 262 | ], 263 | "type": "clip", 264 | }, 265 | ] 266 | `; 267 | 268 | exports[`__getPath should have a path item when clip is called with a path 1`] = ` 269 | [ 270 | { 271 | "props": {}, 272 | "transform": [ 273 | 1, 274 | 0, 275 | 0, 276 | 1, 277 | 0, 278 | 0, 279 | ], 280 | "type": "beginPath", 281 | }, 282 | { 283 | "props": { 284 | "fillRule": "nonzero", 285 | "path": [ 286 | { 287 | "props": { 288 | "anticlockwise": false, 289 | "endAngle": 6.283185307179586, 290 | "radius": 10, 291 | "startAngle": 0, 292 | "x": 100, 293 | "y": 101, 294 | }, 295 | "transform": [ 296 | 1, 297 | 0, 298 | 0, 299 | 1, 300 | 0, 301 | 0, 302 | ], 303 | "type": "arc", 304 | }, 305 | ], 306 | }, 307 | "transform": [ 308 | 1, 309 | 0, 310 | 0, 311 | 1, 312 | 0, 313 | 0, 314 | ], 315 | "type": "clip", 316 | }, 317 | ] 318 | `; 319 | 320 | exports[`__getPath should have a path item when clip is called with a path 2`] = ` 321 | [ 322 | { 323 | "props": {}, 324 | "transform": [ 325 | 1, 326 | 0, 327 | 0, 328 | 1, 329 | 0, 330 | 0, 331 | ], 332 | "type": "beginPath", 333 | }, 334 | { 335 | "props": { 336 | "fillRule": "evenodd", 337 | "path": [ 338 | { 339 | "props": { 340 | "anticlockwise": false, 341 | "endAngle": 6.283185307179586, 342 | "radius": 10, 343 | "startAngle": 0, 344 | "x": 100, 345 | "y": 101, 346 | }, 347 | "transform": [ 348 | 1, 349 | 0, 350 | 0, 351 | 1, 352 | 0, 353 | 0, 354 | ], 355 | "type": "arc", 356 | }, 357 | ], 358 | }, 359 | "transform": [ 360 | 1, 361 | 0, 362 | 0, 363 | 1, 364 | 0, 365 | 0, 366 | ], 367 | "type": "clip", 368 | }, 369 | ] 370 | `; 371 | 372 | exports[`__getPath should have a path item when closePath is called 1`] = ` 373 | [ 374 | { 375 | "props": {}, 376 | "transform": [ 377 | 1, 378 | 0, 379 | 0, 380 | 1, 381 | 0, 382 | 0, 383 | ], 384 | "type": "beginPath", 385 | }, 386 | { 387 | "props": {}, 388 | "transform": [ 389 | 1, 390 | 0, 391 | 0, 392 | 1, 393 | 0, 394 | 0, 395 | ], 396 | "type": "closePath", 397 | }, 398 | ] 399 | `; 400 | 401 | exports[`__getPath should have a path item when ellipse is called 1`] = ` 402 | [ 403 | { 404 | "props": {}, 405 | "transform": [ 406 | 1, 407 | 0, 408 | 0, 409 | 1, 410 | 0, 411 | 0, 412 | ], 413 | "type": "beginPath", 414 | }, 415 | { 416 | "props": { 417 | "anticlockwise": true, 418 | "endAngle": 7, 419 | "radiusX": 3, 420 | "radiusY": 4, 421 | "rotation": 5, 422 | "startAngle": 6, 423 | "x": 1, 424 | "y": 2, 425 | }, 426 | "transform": [ 427 | 1, 428 | 0, 429 | 0, 430 | 1, 431 | 0, 432 | 0, 433 | ], 434 | "type": "ellipse", 435 | }, 436 | ] 437 | `; 438 | 439 | exports[`__getPath should have a path item when lineTo is called 1`] = ` 440 | [ 441 | { 442 | "props": {}, 443 | "transform": [ 444 | 1, 445 | 0, 446 | 0, 447 | 1, 448 | 0, 449 | 0, 450 | ], 451 | "type": "beginPath", 452 | }, 453 | { 454 | "props": { 455 | "x": 1, 456 | "y": 2, 457 | }, 458 | "transform": [ 459 | 1, 460 | 0, 461 | 0, 462 | 1, 463 | 0, 464 | 0, 465 | ], 466 | "type": "lineTo", 467 | }, 468 | ] 469 | `; 470 | 471 | exports[`__getPath should have a path item when moveTo is called 1`] = ` 472 | [ 473 | { 474 | "props": {}, 475 | "transform": [ 476 | 1, 477 | 0, 478 | 0, 479 | 1, 480 | 0, 481 | 0, 482 | ], 483 | "type": "beginPath", 484 | }, 485 | { 486 | "props": { 487 | "x": 1, 488 | "y": 2, 489 | }, 490 | "transform": [ 491 | 1, 492 | 0, 493 | 0, 494 | 1, 495 | 0, 496 | 0, 497 | ], 498 | "type": "moveTo", 499 | }, 500 | ] 501 | `; 502 | 503 | exports[`__getPath should have a path item when quadraticCurveTo is called 1`] = ` 504 | [ 505 | { 506 | "props": {}, 507 | "transform": [ 508 | 1, 509 | 0, 510 | 0, 511 | 1, 512 | 0, 513 | 0, 514 | ], 515 | "type": "beginPath", 516 | }, 517 | ] 518 | `; 519 | 520 | exports[`__getPath should have a path item when rect is called 1`] = ` 521 | [ 522 | { 523 | "props": {}, 524 | "transform": [ 525 | 1, 526 | 0, 527 | 0, 528 | 1, 529 | 0, 530 | 0, 531 | ], 532 | "type": "beginPath", 533 | }, 534 | { 535 | "props": { 536 | "height": 4, 537 | "width": 3, 538 | "x": 1, 539 | "y": 2, 540 | }, 541 | "transform": [ 542 | 1, 543 | 0, 544 | 0, 545 | 1, 546 | 0, 547 | 0, 548 | ], 549 | "type": "rect", 550 | }, 551 | ] 552 | `; 553 | 554 | exports[`__getPath should not have a path item when arc is called with a non-finite number 1`] = ` 555 | [ 556 | { 557 | "props": {}, 558 | "transform": [ 559 | 1, 560 | 0, 561 | 0, 562 | 1, 563 | 0, 564 | 0, 565 | ], 566 | "type": "beginPath", 567 | }, 568 | ] 569 | `; 570 | 571 | exports[`__getPath should not have a path item when arcTo is called with a non-finite number 1`] = ` 572 | [ 573 | { 574 | "props": {}, 575 | "transform": [ 576 | 1, 577 | 0, 578 | 0, 579 | 1, 580 | 0, 581 | 0, 582 | ], 583 | "type": "beginPath", 584 | }, 585 | ] 586 | `; 587 | 588 | exports[`__getPath should not have a path item when bezierCurveTo is called with a non-finite number 1`] = ` 589 | [ 590 | { 591 | "props": {}, 592 | "transform": [ 593 | 1, 594 | 0, 595 | 0, 596 | 1, 597 | 0, 598 | 0, 599 | ], 600 | "type": "beginPath", 601 | }, 602 | ] 603 | `; 604 | 605 | exports[`__getPath should not have a path item when ellipse is called with non-finite numbers 1`] = ` 606 | [ 607 | { 608 | "props": {}, 609 | "transform": [ 610 | 1, 611 | 0, 612 | 0, 613 | 1, 614 | 0, 615 | 0, 616 | ], 617 | "type": "beginPath", 618 | }, 619 | ] 620 | `; 621 | 622 | exports[`__getPath should not have a path item when lineTo is called with non-finite values 1`] = ` 623 | [ 624 | { 625 | "props": {}, 626 | "transform": [ 627 | 1, 628 | 0, 629 | 0, 630 | 1, 631 | 0, 632 | 0, 633 | ], 634 | "type": "beginPath", 635 | }, 636 | ] 637 | `; 638 | 639 | exports[`__getPath should not have a path item when moveTo is called with non-finite values 1`] = ` 640 | [ 641 | { 642 | "props": {}, 643 | "transform": [ 644 | 1, 645 | 0, 646 | 0, 647 | 1, 648 | 0, 649 | 0, 650 | ], 651 | "type": "beginPath", 652 | }, 653 | ] 654 | `; 655 | 656 | exports[`__getPath should not have a path item when quadraticCurveTo is called with non-finite values 1`] = ` 657 | [ 658 | { 659 | "props": {}, 660 | "transform": [ 661 | 1, 662 | 0, 663 | 0, 664 | 1, 665 | 0, 666 | 0, 667 | ], 668 | "type": "beginPath", 669 | }, 670 | ] 671 | `; 672 | 673 | exports[`__getPath should not have a path item when rect is called with non-finite values 1`] = ` 674 | [ 675 | { 676 | "props": {}, 677 | "transform": [ 678 | 1, 679 | 0, 680 | 0, 681 | 1, 682 | 0, 683 | 0, 684 | ], 685 | "type": "beginPath", 686 | }, 687 | ] 688 | `; 689 | 690 | exports[`__getPath should reset the path with beginPath 1`] = ` 691 | [ 692 | { 693 | "props": {}, 694 | "transform": [ 695 | 1, 696 | 0, 697 | 0, 698 | 1, 699 | 0, 700 | 0, 701 | ], 702 | "type": "beginPath", 703 | }, 704 | ] 705 | `; 706 | -------------------------------------------------------------------------------- /src/classes/WebGLRenderingContext.js: -------------------------------------------------------------------------------- 1 | const testFuncs = [ 2 | 'activeTexture', 3 | 'attachShader', 4 | 'bindAttribLocation', 5 | 'bindBuffer', 6 | 'bindFramebuffer', 7 | 'bindRenderbuffer', 8 | 'bindTexture', 9 | 'blendColor', 10 | 'blendEquation', 11 | 'blendEquationSeparate', 12 | 'blendFunc', 13 | 'blendFuncSeparate', 14 | 'bufferData', 15 | 'bufferSubData', 16 | 'checkFramebufferStatus', 17 | 'clear', 18 | 'clearColor', 19 | 'clearDepth', 20 | 'clearStencil', 21 | 'colorMask', 22 | 'compileShader', 23 | 'compressedTexImage2D', 24 | 'compressedTexSubImage2D', 25 | 'copyTexImage2D', 26 | 'copyTexSubImage2D', 27 | 'createBuffer', 28 | 'createFramebuffer', 29 | 'createProgram', 30 | 'createRenderbuffer', 31 | 'createShader', 32 | 'createTexture', 33 | 'cullFace', 34 | 'deleteBuffer', 35 | 'deleteFramebuffer', 36 | 'deleteProgram', 37 | 'deleteRenderbuffer', 38 | 'deleteShader', 39 | 'deleteTexture', 40 | 'depthFunc', 41 | 'depthMask', 42 | 'depthRange', 43 | 'detachShader', 44 | 'disable', 45 | 'disableVertexAttribArray', 46 | 'drawArrays', 47 | 'drawElements', 48 | 'enable', 49 | 'enableVertexAttribArray', 50 | 'finish', 51 | 'flush', 52 | 'framebufferRenderbuffer', 53 | 'framebufferTexture2D', 54 | 'frontFace', 55 | 'generateMipmap', 56 | 'getActiveAttrib', 57 | 'getActiveUniform', 58 | 'getAttachedShaders', 59 | 'getAttribLocation', 60 | 'getBufferParameter', 61 | 'getError', 62 | 'getFramebufferAttachmentParameter', 63 | 'getProgramParameter', 64 | 'getProgramInfoLog', 65 | 'getRenderbufferParameter', 66 | 'getShaderParameter', 67 | 'getShaderInfoLog', 68 | 'getShaderPrecisionFormat', 69 | 'getShaderSource', 70 | 'getSupportedExtensions', 71 | 'getTexParameter', 72 | 'getUniform', 73 | 'getUniformLocation', 74 | 'getVertexAttrib', 75 | 'getVertexAttribOffset', 76 | 'hint', 77 | 'isBuffer', 78 | 'isContextLost', 79 | 'isEnabled', 80 | 'isFramebuffer', 81 | 'isProgram', 82 | 'isRenderbuffer', 83 | 'isShader', 84 | 'isTexture', 85 | 'lineWidth', 86 | 'linkProgram', 87 | 'pixelStorei', 88 | 'polygonOffset', 89 | 'readPixels', 90 | 'renderbufferStorage', 91 | 'sampleCoverage', 92 | 'scissor', 93 | 'shaderSource', 94 | 'stencilFunc', 95 | 'stencilFuncSeparate', 96 | 'stencilMask', 97 | 'stencilMaskSeparate', 98 | 'stencilOp', 99 | 'stencilOpSeparate', 100 | 'texParameterf', 101 | 'texParameteri', 102 | 'texImage2D', 103 | 'texSubImage2D', 104 | 'uniform1f', 105 | 'uniform1fv', 106 | 'uniform1i', 107 | 'uniform1iv', 108 | 'uniform2f', 109 | 'uniform2fv', 110 | 'uniform2i', 111 | 'uniform2iv', 112 | 'uniform3f', 113 | 'uniform3fv', 114 | 'uniform3i', 115 | 'uniform3iv', 116 | 'uniform4f', 117 | 'uniform4fv', 118 | 'uniform4i', 119 | 'uniform4iv', 120 | 'uniformMatrix2fv', 121 | 'uniformMatrix3fv', 122 | 'uniformMatrix4fv', 123 | 'useProgram', 124 | 'validateProgram', 125 | 'vertexAttrib1f', 126 | 'vertexAttrib1fv', 127 | 'vertexAttrib2f', 128 | 'vertexAttrib2fv', 129 | 'vertexAttrib3f', 130 | 'vertexAttrib3fv', 131 | 'vertexAttrib4f', 132 | 'vertexAttrib4fv', 133 | 'vertexAttribPointer', 134 | 'viewport', 135 | ]; 136 | const mockEnums = { 137 | DEPTH_BUFFER_BIT: 256, 138 | STENCIL_BUFFER_BIT: 1024, 139 | COLOR_BUFFER_BIT: 16384, 140 | POINTS: 0, 141 | LINES: 1, 142 | LINE_LOOP: 2, 143 | LINE_STRIP: 3, 144 | TRIANGLES: 4, 145 | TRIANGLE_STRIP: 5, 146 | TRIANGLE_FAN: 6, 147 | ZERO: 0, 148 | ONE: 1, 149 | SRC_COLOR: 768, 150 | ONE_MINUS_SRC_COLOR: 769, 151 | SRC_ALPHA: 770, 152 | ONE_MINUS_SRC_ALPHA: 771, 153 | DST_ALPHA: 772, 154 | ONE_MINUS_DST_ALPHA: 773, 155 | DST_COLOR: 774, 156 | ONE_MINUS_DST_COLOR: 775, 157 | SRC_ALPHA_SATURATE: 776, 158 | FUNC_ADD: 32774, 159 | BLEND_EQUATION: 32777, 160 | BLEND_EQUATION_RGB: 32777, 161 | BLEND_EQUATION_ALPHA: 34877, 162 | FUNC_SUBTRACT: 32778, 163 | FUNC_REVERSE_SUBTRACT: 32779, 164 | BLEND_DST_RGB: 32968, 165 | BLEND_SRC_RGB: 32969, 166 | BLEND_DST_ALPHA: 32970, 167 | BLEND_SRC_ALPHA: 32971, 168 | CONSTANT_COLOR: 32769, 169 | ONE_MINUS_CONSTANT_COLOR: 32770, 170 | CONSTANT_ALPHA: 32771, 171 | ONE_MINUS_CONSTANT_ALPHA: 32772, 172 | BLEND_COLOR: 32773, 173 | ARRAY_BUFFER: 34962, 174 | ELEMENT_ARRAY_BUFFER: 34963, 175 | ARRAY_BUFFER_BINDING: 34964, 176 | ELEMENT_ARRAY_BUFFER_BINDING: 34965, 177 | STREAM_DRAW: 35040, 178 | STATIC_DRAW: 35044, 179 | DYNAMIC_DRAW: 35048, 180 | BUFFER_SIZE: 34660, 181 | BUFFER_USAGE: 34661, 182 | CURRENT_VERTEX_ATTRIB: 34342, 183 | FRONT: 1028, 184 | BACK: 1029, 185 | FRONT_AND_BACK: 1032, 186 | TEXTURE_2D: 3553, 187 | CULL_FACE: 2884, 188 | BLEND: 3042, 189 | DITHER: 3024, 190 | STENCIL_TEST: 2960, 191 | DEPTH_TEST: 2929, 192 | SCISSOR_TEST: 3089, 193 | POLYGON_OFFSET_FILL: 32823, 194 | SAMPLE_ALPHA_TO_COVERAGE: 32926, 195 | SAMPLE_COVERAGE: 32928, 196 | NO_ERROR: 0, 197 | INVALID_ENUM: 1280, 198 | INVALID_VALUE: 1281, 199 | INVALID_OPERATION: 1282, 200 | OUT_OF_MEMORY: 1285, 201 | CW: 2304, 202 | CCW: 2305, 203 | LINE_WIDTH: 2849, 204 | ALIASED_POINT_SIZE_RANGE: 33901, 205 | ALIASED_LINE_WIDTH_RANGE: 33902, 206 | CULL_FACE_MODE: 2885, 207 | FRONT_FACE: 2886, 208 | DEPTH_RANGE: 2928, 209 | DEPTH_WRITEMASK: 2930, 210 | DEPTH_CLEAR_VALUE: 2931, 211 | DEPTH_FUNC: 2932, 212 | STENCIL_CLEAR_VALUE: 2961, 213 | STENCIL_FUNC: 2962, 214 | STENCIL_FAIL: 2964, 215 | STENCIL_PASS_DEPTH_FAIL: 2965, 216 | STENCIL_PASS_DEPTH_PASS: 2966, 217 | STENCIL_REF: 2967, 218 | STENCIL_VALUE_MASK: 2963, 219 | STENCIL_WRITEMASK: 2968, 220 | STENCIL_BACK_FUNC: 34816, 221 | STENCIL_BACK_FAIL: 34817, 222 | STENCIL_BACK_PASS_DEPTH_FAIL: 34818, 223 | STENCIL_BACK_PASS_DEPTH_PASS: 34819, 224 | STENCIL_BACK_REF: 36003, 225 | STENCIL_BACK_VALUE_MASK: 36004, 226 | STENCIL_BACK_WRITEMASK: 36005, 227 | VIEWPORT: 2978, 228 | SCISSOR_BOX: 3088, 229 | COLOR_CLEAR_VALUE: 3106, 230 | COLOR_WRITEMASK: 3107, 231 | UNPACK_ALIGNMENT: 3317, 232 | PACK_ALIGNMENT: 3333, 233 | MAX_TEXTURE_SIZE: 3379, 234 | MAX_VIEWPORT_DIMS: 3386, 235 | SUBPIXEL_BITS: 3408, 236 | RED_BITS: 3410, 237 | GREEN_BITS: 3411, 238 | BLUE_BITS: 3412, 239 | ALPHA_BITS: 3413, 240 | DEPTH_BITS: 3414, 241 | STENCIL_BITS: 3415, 242 | POLYGON_OFFSET_UNITS: 10752, 243 | POLYGON_OFFSET_FACTOR: 32824, 244 | TEXTURE_BINDING_2D: 32873, 245 | SAMPLE_BUFFERS: 32936, 246 | SAMPLES: 32937, 247 | SAMPLE_COVERAGE_VALUE: 32938, 248 | SAMPLE_COVERAGE_INVERT: 32939, 249 | COMPRESSED_TEXTURE_FORMATS: 34467, 250 | DONT_CARE: 4352, 251 | FASTEST: 4353, 252 | NICEST: 4354, 253 | GENERATE_MIPMAP_HINT: 33170, 254 | BYTE: 5120, 255 | UNSIGNED_BYTE: 5121, 256 | SHORT: 5122, 257 | UNSIGNED_SHORT: 5123, 258 | INT: 5124, 259 | UNSIGNED_INT: 5125, 260 | FLOAT: 5126, 261 | DEPTH_COMPONENT: 6402, 262 | ALPHA: 6406, 263 | RGB: 6407, 264 | RGBA: 6408, 265 | LUMINANCE: 6409, 266 | LUMINANCE_ALPHA: 6410, 267 | UNSIGNED_SHORT_4_4_4_4: 32819, 268 | UNSIGNED_SHORT_5_5_5_1: 32820, 269 | UNSIGNED_SHORT_5_6_5: 33635, 270 | FRAGMENT_SHADER: 35632, 271 | VERTEX_SHADER: 35633, 272 | MAX_VERTEX_ATTRIBS: 34921, 273 | MAX_VERTEX_UNIFORM_VECTORS: 36347, 274 | MAX_VARYING_VECTORS: 36348, 275 | MAX_COMBINED_TEXTURE_IMAGE_UNITS: 35661, 276 | MAX_VERTEX_TEXTURE_IMAGE_UNITS: 35660, 277 | MAX_TEXTURE_IMAGE_UNITS: 34930, 278 | MAX_FRAGMENT_UNIFORM_VECTORS: 36349, 279 | SHADER_TYPE: 35663, 280 | DELETE_STATUS: 35712, 281 | LINK_STATUS: 35714, 282 | VALIDATE_STATUS: 35715, 283 | ATTACHED_SHADERS: 35717, 284 | ACTIVE_UNIFORMS: 35718, 285 | ACTIVE_ATTRIBUTES: 35721, 286 | SHADING_LANGUAGE_VERSION: 35724, 287 | CURRENT_PROGRAM: 35725, 288 | NEVER: 512, 289 | LESS: 513, 290 | EQUAL: 514, 291 | LEQUAL: 515, 292 | GREATER: 516, 293 | NOTEQUAL: 517, 294 | GEQUAL: 518, 295 | ALWAYS: 519, 296 | KEEP: 7680, 297 | REPLACE: 7681, 298 | INCR: 7682, 299 | DECR: 7683, 300 | INVERT: 5386, 301 | INCR_WRAP: 34055, 302 | DECR_WRAP: 34056, 303 | VENDOR: 7936, 304 | RENDERER: 7937, 305 | VERSION: 7938, 306 | NEAREST: 9728, 307 | LINEAR: 9729, 308 | NEAREST_MIPMAP_NEAREST: 9984, 309 | LINEAR_MIPMAP_NEAREST: 9985, 310 | NEAREST_MIPMAP_LINEAR: 9986, 311 | LINEAR_MIPMAP_LINEAR: 9987, 312 | TEXTURE_MAG_FILTER: 10240, 313 | TEXTURE_MIN_FILTER: 10241, 314 | TEXTURE_WRAP_S: 10242, 315 | TEXTURE_WRAP_T: 10243, 316 | TEXTURE: 5890, 317 | TEXTURE_CUBE_MAP: 34067, 318 | TEXTURE_BINDING_CUBE_MAP: 34068, 319 | TEXTURE_CUBE_MAP_POSITIVE_X: 34069, 320 | TEXTURE_CUBE_MAP_NEGATIVE_X: 34070, 321 | TEXTURE_CUBE_MAP_POSITIVE_Y: 34071, 322 | TEXTURE_CUBE_MAP_NEGATIVE_Y: 34072, 323 | TEXTURE_CUBE_MAP_POSITIVE_Z: 34073, 324 | TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, 325 | MAX_CUBE_MAP_TEXTURE_SIZE: 34076, 326 | TEXTURE0: 33984, 327 | TEXTURE1: 33985, 328 | TEXTURE2: 33986, 329 | TEXTURE3: 33987, 330 | TEXTURE4: 33988, 331 | TEXTURE5: 33989, 332 | TEXTURE6: 33990, 333 | TEXTURE7: 33991, 334 | TEXTURE8: 33992, 335 | TEXTURE9: 33993, 336 | TEXTURE10: 33994, 337 | TEXTURE11: 33995, 338 | TEXTURE12: 33996, 339 | TEXTURE13: 33997, 340 | TEXTURE14: 33998, 341 | TEXTURE15: 33999, 342 | TEXTURE16: 34000, 343 | TEXTURE17: 34001, 344 | TEXTURE18: 34002, 345 | TEXTURE19: 34003, 346 | TEXTURE20: 34004, 347 | TEXTURE21: 34005, 348 | TEXTURE22: 34006, 349 | TEXTURE23: 34007, 350 | TEXTURE24: 34008, 351 | TEXTURE25: 34009, 352 | TEXTURE26: 34010, 353 | TEXTURE27: 34011, 354 | TEXTURE28: 34012, 355 | TEXTURE29: 34013, 356 | TEXTURE30: 34014, 357 | TEXTURE31: 34015, 358 | ACTIVE_TEXTURE: 34016, 359 | REPEAT: 10497, 360 | CLAMP_TO_EDGE: 33071, 361 | MIRRORED_REPEAT: 33648, 362 | FLOAT_VEC2: 35664, 363 | FLOAT_VEC3: 35665, 364 | FLOAT_VEC4: 35666, 365 | INT_VEC2: 35667, 366 | INT_VEC3: 35668, 367 | INT_VEC4: 35669, 368 | BOOL: 35670, 369 | BOOL_VEC2: 35671, 370 | BOOL_VEC3: 35672, 371 | BOOL_VEC4: 35673, 372 | FLOAT_MAT2: 35674, 373 | FLOAT_MAT3: 35675, 374 | FLOAT_MAT4: 35676, 375 | SAMPLER_2D: 35678, 376 | SAMPLER_CUBE: 35680, 377 | VERTEX_ATTRIB_ARRAY_ENABLED: 34338, 378 | VERTEX_ATTRIB_ARRAY_SIZE: 34339, 379 | VERTEX_ATTRIB_ARRAY_STRIDE: 34340, 380 | VERTEX_ATTRIB_ARRAY_TYPE: 34341, 381 | VERTEX_ATTRIB_ARRAY_NORMALIZED: 34922, 382 | VERTEX_ATTRIB_ARRAY_POINTER: 34373, 383 | VERTEX_ATTRIB_ARRAY_BUFFER_BINDING: 34975, 384 | IMPLEMENTATION_COLOR_READ_TYPE: 35738, 385 | IMPLEMENTATION_COLOR_READ_FORMAT: 35739, 386 | COMPILE_STATUS: 35713, 387 | LOW_FLOAT: 36336, 388 | MEDIUM_FLOAT: 36337, 389 | HIGH_FLOAT: 36338, 390 | LOW_INT: 36339, 391 | MEDIUM_INT: 36340, 392 | HIGH_INT: 36341, 393 | FRAMEBUFFER: 36160, 394 | RENDERBUFFER: 36161, 395 | RGBA4: 32854, 396 | RGB5_A1: 32855, 397 | RGB565: 36194, 398 | DEPTH_COMPONENT16: 33189, 399 | STENCIL_INDEX: 6401, 400 | STENCIL_INDEX8: 36168, 401 | DEPTH_STENCIL: 34041, 402 | RENDERBUFFER_WIDTH: 36162, 403 | RENDERBUFFER_HEIGHT: 36163, 404 | RENDERBUFFER_INTERNAL_FORMAT: 36164, 405 | RENDERBUFFER_RED_SIZE: 36176, 406 | RENDERBUFFER_GREEN_SIZE: 36177, 407 | RENDERBUFFER_BLUE_SIZE: 36178, 408 | RENDERBUFFER_ALPHA_SIZE: 36179, 409 | RENDERBUFFER_DEPTH_SIZE: 36180, 410 | RENDERBUFFER_STENCIL_SIZE: 36181, 411 | FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE: 36048, 412 | FRAMEBUFFER_ATTACHMENT_OBJECT_NAME: 36049, 413 | FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL: 36050, 414 | FRAMEBUFFER_ATTACHMENT_TEXTURE_CUBE_MAP_FACE: 36051, 415 | COLOR_ATTACHMENT0: 36064, 416 | DEPTH_ATTACHMENT: 36096, 417 | STENCIL_ATTACHMENT: 36128, 418 | DEPTH_STENCIL_ATTACHMENT: 33306, 419 | NONE: 0, 420 | FRAMEBUFFER_COMPLETE: 36053, 421 | FRAMEBUFFER_INCOMPLETE_ATTACHMENT: 36054, 422 | FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: 36055, 423 | FRAMEBUFFER_INCOMPLETE_DIMENSIONS: 36057, 424 | FRAMEBUFFER_UNSUPPORTED: 36061, 425 | FRAMEBUFFER_BINDING: 36006, 426 | RENDERBUFFER_BINDING: 36007, 427 | MAX_RENDERBUFFER_SIZE: 34024, 428 | INVALID_FRAMEBUFFER_OPERATION: 1286, 429 | UNPACK_FLIP_Y_WEBGL: 37440, 430 | UNPACK_PREMULTIPLY_ALPHA_WEBGL: 37441, 431 | CONTEXT_LOST_WEBGL: 37442, 432 | UNPACK_COLORSPACE_CONVERSION_WEBGL: 37443, 433 | BROWSER_DEFAULT_WEBGL: 37444, 434 | }; 435 | 436 | const mockExtensions = { 437 | // ratified 438 | OES_texture_float: {}, 439 | OES_texture_half_float: {}, 440 | WEBGL_lose_context: { 441 | restoreContext() {}, 442 | loseContext() {}, 443 | }, 444 | OES_standard_derivatives: {}, 445 | OES_vertex_array_object: { 446 | createVertexArrayOES() {}, 447 | bindVertexArrayOES(param) {}, 448 | deleteVertexArrayOES(param) {}, 449 | }, 450 | WEBGL_debug_renderer_info: null, 451 | WEBGL_debug_shaders: null, 452 | WEBGL_compressed_texture_s3tc: null, 453 | WEBGL_depth_texture: {}, 454 | OES_element_index_uint: {}, 455 | EXT_texture_filter_anisotropic: null, 456 | EXT_frag_depth: {}, 457 | WEBGL_draw_buffers: {}, 458 | ANGLE_instanced_arrays: null, 459 | OES_texture_float_linear: null, 460 | OES_texture_half_float_linear: null, 461 | EXT_blend_minmax: {}, 462 | EXT_shader_texture_lod: null, 463 | // community 464 | WEBGL_compressed_texture_atc: null, 465 | WEBGL_compressed_texture_pvrtc: null, 466 | EXT_color_buffer_half_float: null, 467 | WEBGL_color_buffer_float: null, 468 | EXT_sRGB: null, 469 | WEBGL_compressed_texture_etc1: null, 470 | }; 471 | 472 | const mockParams = { 473 | [mockEnums['MAX_TEXTURE_IMAGE_UNITS']]: 16, 474 | [mockEnums['VERSION']]: 'WebGL 2.0 (OpenGL ES 3.0 Chromium)', 475 | [mockEnums['SCISSOR_BOX']]: new Int32Array([0, 0, 300, 150]), 476 | [mockEnums['VIEWPORT']]: new Int32Array([0, 0, 300, 150]), 477 | }; 478 | 479 | export default class WebGLRenderingContext { 480 | constructor(canvas, contextAttributes) { 481 | this._contextAttributes = contextAttributes; 482 | 483 | testFuncs.forEach((key) => { 484 | WebGLRenderingContext.prototype[key] = function () { 485 | return key === 'getSupportedExtensions' ? [] : {}; 486 | }; 487 | }); 488 | 489 | Object.keys(mockEnums).forEach((key) => { 490 | WebGLRenderingContext.prototype[key] = mockEnums[key]; 491 | }); 492 | 493 | this._canvas = canvas; 494 | } 495 | 496 | getExtension(ext) { 497 | return mockExtensions[ext]; 498 | } 499 | 500 | getParameter(param) { 501 | return mockParams[param]; 502 | } 503 | 504 | getContextAttributes() { 505 | return this._contextAttributes; 506 | } 507 | 508 | getShaderPrecisionFormat() { 509 | return { 510 | rangeMin: 127, 511 | rangeMax: 127, 512 | precision: 23, 513 | }; 514 | } 515 | } 516 | --------------------------------------------------------------------------------