HTML Canvas 单元测试

发布于 2024-10-06 19:47:44 字数 52 浏览 3 评论 0原文

如何对在 HTML 画布上绘制的 Javascript 进行单元测试?应检查画布上的绘图。

How can I unit-test Javascript that draws on an HTML canvas? Drawing on the canvas should be checked.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

九公里浅绿 2024-10-13 19:47:44

我使用 Jasmine 和 js-imagediff 编写了一个示例,用于对画布和其他图像类型进行单元测试。

Jasmine Canvas 单元测试

我发现这比确保特定方法更好已调用模拟 Canvas,因为不同系列的方法可能会产生相同的方法。通常,我会创建具有预期值的画布,或使用已知稳定版本的代码来测试开发版本。

I wrote an example for unit-testing canvas and other image-y types with Jasmine and js-imagediff.

Jasmine Canvas Unit Testing

I find this to be better than making sure specific methods on a mock Canvas have been invoked because different series of methods may produce the same method. Typically, I will create a canvas with the expected value or use a known-stable version of the code to test a development version against.

dawn曙光 2024-10-13 19:47:44

正如问题评论中所讨论的,检查是否使用合适的参数调用了某些函数非常重要。 pcjuzer提出了代理模式的使用。以下示例(RightJS 代码)显示了执行此操作的一种方法:

var Context = new Class({
    initialize: function($canvasElem) {
        this._ctx = $canvasElem._.getContext('2d');

        this._calls = []; // names/args of recorded calls

        this._initMethods();
    },
    _initMethods: function() {
        // define methods to test here
        // no way to introspect so we have to do some extra work :(
        var methods = {
            fill: function() {
                this._ctx.fill();
            },
            lineTo: function(x, y) {
                this._ctx.lineTo(x, y);
            },
            moveTo: function(x, y) {
                this._ctx.moveTo(x, y);
            },
            stroke: function() {
                this._ctx.stroke();
            }
            // and so on
        };

        // attach methods to the class itself
        var scope = this;
        var addMethod = function(name, method) {
            scope[methodName] = function() {
                scope.record(name, arguments);

                method.apply(scope, arguments);
            };
        }

        for(var methodName in methods) {
            var method = methods[methodName];

            addMethod(methodName, method);
        }
    },
    assign: function(k, v) {
        this._ctx[k] = v;
    },
    record: function(methodName, args) {
        this._calls.push({name: methodName, args: args});
    },
    getCalls: function() {
        return this._calls;
    }
    // TODO: expand API as needed
});

// Usage
var ctx = new Context($('myCanvas'));

ctx.moveTo(34, 54);
ctx.lineTo(63, 12);

ctx.assign('strokeStyle', "#FF00FF");
ctx.stroke();

var calls = ctx.getCalls();

console.log(calls);

您可以在此处找到功能演示。

我使用了类似的模式来实现 API 中缺少的一些功能。您可能需要对其进行一些修改才能满足您的目的。祝你好运!

As discussed in the question comments it's important to check that certain functions have been invoked with suitable parameters. pcjuzer proposed the usage of proxy pattern. The following example (RightJS code) shows one way to do this:

var Context = new Class({
    initialize: function($canvasElem) {
        this._ctx = $canvasElem._.getContext('2d');

        this._calls = []; // names/args of recorded calls

        this._initMethods();
    },
    _initMethods: function() {
        // define methods to test here
        // no way to introspect so we have to do some extra work :(
        var methods = {
            fill: function() {
                this._ctx.fill();
            },
            lineTo: function(x, y) {
                this._ctx.lineTo(x, y);
            },
            moveTo: function(x, y) {
                this._ctx.moveTo(x, y);
            },
            stroke: function() {
                this._ctx.stroke();
            }
            // and so on
        };

        // attach methods to the class itself
        var scope = this;
        var addMethod = function(name, method) {
            scope[methodName] = function() {
                scope.record(name, arguments);

                method.apply(scope, arguments);
            };
        }

        for(var methodName in methods) {
            var method = methods[methodName];

            addMethod(methodName, method);
        }
    },
    assign: function(k, v) {
        this._ctx[k] = v;
    },
    record: function(methodName, args) {
        this._calls.push({name: methodName, args: args});
    },
    getCalls: function() {
        return this._calls;
    }
    // TODO: expand API as needed
});

// Usage
var ctx = new Context($('myCanvas'));

ctx.moveTo(34, 54);
ctx.lineTo(63, 12);

ctx.assign('strokeStyle', "#FF00FF");
ctx.stroke();

var calls = ctx.getCalls();

console.log(calls);

You can find a functional demo here.

I have used a similar pattern to implement some features missing from the API. You might need to hack it a bit to fit your purposes. Good luck!

爱*していゐ 2024-10-13 19:47:44

我制作非常简单的画布并用摩卡测试它们。我的做法与 Juho Vepsäläinen 类似,但我的看起来更简单一些。我是在ec2015写的。

CanvasMock 类:

import ContextMock from './ContextMock.js'

export default class {
  constructor (width, height)
  {
    this.mock = [];
    this.width = width;
    this.height = height;
    this.context = new ContextMock(this.mock);
  }

  getContext (string)
  {
    this.mock.push('[getContext ' + string + ']')
    return this.context
  }
}

ContextMock 类:

export default class {
  constructor(mock)
  {
    this.mock = mock
  }

  beginPath()
  {
    this.mock.push('[beginPath]')
  }

  moveTo(x, y)
  {
    this.mock.push('[moveTo ' + x + ', ' + y + ']')
  }

  lineTo(x, y)
  {
    this.mock.push('[lineTo ' + x + ', ' + y + ']')
  }

  stroke()
  {
    this.mock.push('[stroke]')
  }
}

一些评估模拟本身功能的摩卡测试:

describe('CanvasMock and ContextMock', ()=> {
    it('should be able to return width and height', ()=> {
      let canvas = new CanvasMock(500,600)
      assert.equal(canvas.width, 500)
      assert.equal(canvas.height, 600)
    })
    it('should be able to update mock for getContext', ()=> {
      let canvas = new CanvasMock(500,600)
      let ctx = canvas.getContext('2d')
      assert.equal(canvas.mock, '[getContext 2d]')
    })
})

评估返回画布的函数功能的摩卡测试:

import Myfunction from 'MyFunction.js'

describe('MyFuntion', ()=> {
it('should be able to return correct canvas', ()=> {
  let testCanvas = new CanvasMock(500,600)
  let ctx = testCanvas.getContext('2d')
  ctx.beginPath()
  ctx.moveTo(0,0)
  ctx.lineTo(8,8)
  ctx.stroke()
  assert.deepEqual(MyFunction(new CanvasMock(500,600), 8, 8), canvas.mock, [ '[getContext 2d]', '[beginPath]', '[moveTo 0, 0]', [lineTo 8, 8]', '[stroke]' ])
})

因此在本例中 myfunction 将您传入的画布作为参数( Myfunction( new CanvasMock(500,600), 8, 8) ) 并在其上写一行从 0,0 到您作为参数传入的内容 ( Myfunction(new CanvasMock(500,600),** 8, 8 **) ),然后返回编辑后的画布。

因此,当您在现实生活中使用该函数时,您可以传入实际的画布,而不是画布模拟,然后它将运行相同的方法,但执行实际的画布操作。

阅读有关模拟的内容 这里

I make really simple canvases and test them with mocha. I do it similarly to Juho Vepsäläinen but mine looks a little simpler. I wrote it in ec2015.

CanvasMock class:

import ContextMock from './ContextMock.js'

export default class {
  constructor (width, height)
  {
    this.mock = [];
    this.width = width;
    this.height = height;
    this.context = new ContextMock(this.mock);
  }

  getContext (string)
  {
    this.mock.push('[getContext ' + string + ']')
    return this.context
  }
}

ContextMock class:

export default class {
  constructor(mock)
  {
    this.mock = mock
  }

  beginPath()
  {
    this.mock.push('[beginPath]')
  }

  moveTo(x, y)
  {
    this.mock.push('[moveTo ' + x + ', ' + y + ']')
  }

  lineTo(x, y)
  {
    this.mock.push('[lineTo ' + x + ', ' + y + ']')
  }

  stroke()
  {
    this.mock.push('[stroke]')
  }
}

some mocha tests that evaluates the functionality of the mock itself:

describe('CanvasMock and ContextMock', ()=> {
    it('should be able to return width and height', ()=> {
      let canvas = new CanvasMock(500,600)
      assert.equal(canvas.width, 500)
      assert.equal(canvas.height, 600)
    })
    it('should be able to update mock for getContext', ()=> {
      let canvas = new CanvasMock(500,600)
      let ctx = canvas.getContext('2d')
      assert.equal(canvas.mock, '[getContext 2d]')
    })
})

A mocha tests that evaluates the functionality of a function that returns a canvas:

import Myfunction from 'MyFunction.js'

describe('MyFuntion', ()=> {
it('should be able to return correct canvas', ()=> {
  let testCanvas = new CanvasMock(500,600)
  let ctx = testCanvas.getContext('2d')
  ctx.beginPath()
  ctx.moveTo(0,0)
  ctx.lineTo(8,8)
  ctx.stroke()
  assert.deepEqual(MyFunction(new CanvasMock(500,600), 8, 8), canvas.mock, [ '[getContext 2d]', '[beginPath]', '[moveTo 0, 0]', [lineTo 8, 8]', '[stroke]' ])
})

so in this example myfunction takes the canvas you passed in as an argument ( Myfunction(new CanvasMock(500,600), 8, 8) ) and writes a line on it from 0,0 to whatever you pass in as the arguments ( Myfunction(new CanvasMock(500,600),** 8, 8**) ) and then returns the edited canvas.

so when you use the function in real life you can pass in an actual canvas, not a canvas mock and then it will run those same methods but do actual canvas things.

read about mocks here

酒解孤独 2024-10-13 19:47:44

由于画布上绘制的“形状”和“线条”不是实际的对象(就像纸上的墨水),因此很难(不可能?)对其进行正常的单元测试。

您可以使用标准画布做的最好的事情是分析像素数据(来自 putImageData/getImageData。就像 bedraw 所说的那样)。

现在,我还没有尝试过这个,但它可能更符合您的需要。 Cake 是一个画布库。它使用了大量的 putImageData/getImageData。 此示例可能有助于您尝试进行测试。

希望有助于回答您的问题。

Since the "shapes" and "lines" drawn on a canvas are not actual objects (it's like ink on paper), it would be very hard (impossible?) to do a normal unit test on that.

The best you can do with standard canvas it analyze the pixel data (from the putImageData/getImageData. Like what bedraw was saying).

Now, I haven't tried this yet, but it might be more what you need. Cake is a library for the canvas. It's using alot of the putImageData/getImageData. This example might help with what you are trying to do with a test.

Hope that helps answer your question.

于我来说 2024-10-13 19:47:44

我最近一直在研究画布测试,现在我想到了一个页面,该页面允许将画布与画布应有的“已知良好”图像版本进行比较。这将使视觉比较变得快速而容易。

也许有一个按钮,假设输出正常,更新服务器上的图像版本(通过向其发送 toDataUrl() 输出)。这个新版本可以用于将来的比较。

不完全(完全)自动化 - 但它确实使比较代码的输出变得容易。

编辑:

现在我做了这个:

测试画布输出的实用程序

左边的图表是真正的画布,而右侧是存储在数据库中的图像,它应该是什么样子(取自当我知道代码正在运行时)。将有很多这些来测试我的代码的所有(最终)方面。

I've been looking at canvas testing recently and I've now thought about a page that allows comparing the canvas to a "known good" image version of what the canvas should look like. This would make a visual comparison quick and easy.

And maybe have a button that, assuming the output is OK, updates the image version on the server (by sending the toDataUrl() output to it). This new version can then be used for future comparisons.

Not exactly (at all) automated - but it does make comparing the output of your code easy.

Edit:

Now I've made this:

Utility to test canvas output

The left chart is the real canvas whilst the right is an image stored in a database of what it should look like (taken from when I know the code is working). There'll be lots of these to test all (eventually) aspects of my code.

计㈡愣 2024-10-13 19:47:44

从开发人员的角度来看,画布几乎是只写的,因为一旦绘制,就很难以编程方式获取有用的东西。当然,我们可以进行逐点识别,但这太乏味了,而且这样的测试很难编写和维护。

最好拦截对画布对象的调用并进行调查。以下是一些选项:

  1. 创建一个记录所有调用的包装对象。 Juho Vepsäläinen 发布了一个这样的例子。
  2. 如果可能的话,使用像 frabric.js 这样的库,它可以提供更高级别的绘图抽象。 “绘图”是可以直接检查的 JS 对象,也可以转换为更易于检查和测试的 SVG。
  3. 使用 Canteen 拦截画布对象的所有函数调用和属性更改。这与选项 1 类似。
  4. 使用 Canteen 和 rabbit ,它为您提供了一些 Jasmine 自定义尺寸匹配器对齐和 getBBox() 函数,可用于确定画布上绘制的内容的大小和位置。

From a developer's point of view the canvas is almost write-only because once drawn it's difficult to programmatically get something useful back. Sure one can do a point by point recognition but that's too tedious and such tests are hard to be written and maintained.

It's better to intercept the calls made to a canvas object and investigate those. Here are a few options:

  1. Create a wrapper object that records all the calls. Juho Vepsäläinen posted a such example.
  2. If possible use a library like frabric.js that offers a higher level of abstraction for drawing. The "drawings" are JS objects that can be inspected directly or converted to SVG which is easier to inspect and test.
  3. Use Canteen to intercept all the function calls and attribute changes of a canvas object. This is similar with option 1.
  4. Use Canteen with rabbit which offers you a few Jasmine custom matchers for size and alignment and a function getBBox() that can be used to determine the size and the position of the stuff being drawn on the canvas.
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文