| <!DOCTYPE html> |
| <html> |
| <head> |
| <script src="../../http/tests/inspector/resources/inspector-test.js"></script> |
| <script src="resources/recording-utilities.js"></script> |
| <script> |
| let ctx = null; |
| |
| // 2x2 red square |
| let image = document.createElement("img"); |
| image.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAAXNSR0IArs4c6QAAABNJREFUCB1j/M/AAEQMDEwgAgQAHxcCAmtAm/sAAAAASUVORK5CYII="; |
| |
| // Invalid video |
| let video = document.createElement("video"); |
| |
| // Blank canvas |
| let canvas = document.createElement("canvas"); |
| canvas.width = 2; |
| canvas.height = 2; |
| |
| let linearGradient = null; |
| |
| let radialGradient = null; |
| |
| let pattern = null; |
| |
| let path12 = new Path2D("M 1 2"); |
| let path34 = new Path2D("M 3 4"); |
| |
| let imageData14 = new ImageData(1, 4); |
| let imageData23 = new ImageData(2, 3); |
| |
| let bitmap = null; |
| |
| async function load() { |
| ctx = canvas.getContext("2d"); |
| linearGradient = ctx.createLinearGradient(1, 2, 3, 4); |
| radialGradient = ctx.createRadialGradient(1, 2, 3, 4, 5, 6); |
| pattern = ctx.createPattern(image, "no-repeat"); |
| bitmap = await createImageBitmap(image); |
| |
| ctx.save(); |
| cancelActions(); |
| |
| runTest(); |
| } |
| |
| function ignoreException(func){ |
| try { |
| func(); |
| } catch (e) { } |
| } |
| |
| let requestAnimationFrameId = NaN; |
| let saveCount = 1; |
| |
| function cancelActions() { |
| for (let i = 0; i < saveCount; ++i) |
| ctx.restore(); |
| ctx.restore(); // Ensures the state is reset between test cases. |
| |
| cancelAnimationFrame(requestAnimationFrameId); |
| requestAnimationFrameId = NaN; |
| |
| ctx.save(); // Ensures the state is reset between test cases. |
| ctx.save(); // This matches the `restore` call in `performActions`. |
| saveCount = 1; |
| |
| ctx.resetTransform(); |
| ctx.beginPath(); |
| ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); |
| } |
| |
| function performActions() { |
| let frames = [ |
| () => { |
| ignoreException(() => ctx.arc(1, 2, 3, 4, 5)); |
| ignoreException(() => ctx.arc(6, 7, 8, 9, 10, true)); |
| }, |
| () => { |
| ignoreException(() => ctx.arcTo(1, 2, 3, 4, 5)); |
| }, |
| () => { |
| ctx.beginPath(); |
| }, |
| () => { |
| ctx.bezierCurveTo(1, 2, 3, 4, 5, 6); |
| }, |
| () => { |
| ctx.clearRect(1, 2, 3, 4); |
| }, |
| () => { |
| ctx.clearShadow(); |
| }, |
| () => { |
| ctx.clip(); |
| ctx.clip("evenodd"); |
| ctx.clip(path12); |
| ctx.clip(path34, "evenodd"); |
| }, |
| () => { |
| ctx.closePath(); |
| }, |
| () => { |
| ignoreException(() => ctx.createImageData(imageData14)); |
| ignoreException(() => ctx.createImageData(2, 3)); |
| }, |
| () => { |
| ignoreException(() => ctx.createLinearGradient(1, 2, 3, 4)); |
| }, |
| () => { |
| ignoreException(() => ctx.createPattern(image, "testA")); |
| ignoreException(() => ctx.createPattern(video, "testB")); |
| ignoreException(() => ctx.createPattern(canvas, "testC")); |
| ignoreException(() => ctx.createPattern(bitmap, "testD")); |
| }, |
| () => { |
| ignoreException(() => ctx.createRadialGradient(1, 2, 3, 4, 5, 6)); |
| }, |
| () => { |
| ctx.direction; |
| ctx.direction = "test"; |
| }, |
| () => { |
| ctx.drawFocusIfNeeded(document.body); |
| ctx.drawFocusIfNeeded(path12, document.body); |
| }, |
| () => { |
| ignoreException(() => ctx.drawImage(image, 11, 12)); |
| ignoreException(() => ctx.drawImage(image, 13, 14, 15, 16)); |
| ignoreException(() => ctx.drawImage(image, 17, 18, 19, 110, 111, 112, 113, 114)); |
| |
| ignoreException(() => ctx.drawImage(video, 21, 22)); |
| ignoreException(() => ctx.drawImage(video, 23, 24, 25, 26)); |
| ignoreException(() => ctx.drawImage(video, 27, 28, 29, 210, 211, 212, 213, 214)); |
| |
| ignoreException(() => ctx.drawImage(canvas, 31, 32)); |
| ignoreException(() => ctx.drawImage(canvas, 33, 34, 35, 36)); |
| ignoreException(() => ctx.drawImage(canvas, 37, 38, 39, 310, 311, 312, 313, 314)); |
| |
| ignoreException(() => ctx.drawImage(bitmap, 41, 42)); |
| ignoreException(() => ctx.drawImage(bitmap, 43, 44, 45, 46)); |
| ignoreException(() => ctx.drawImage(bitmap, 47, 48, 49, 410, 411, 412, 413, 414)); |
| }, |
| () => { |
| ctx.drawImageFromRect(image, 1, 2, 3, 4, 5, 6, 7, 8) |
| ctx.drawImageFromRect(image, 9, 10, 11, 12, 13, 14, 15, 16, "test"); |
| }, |
| () => { |
| ignoreException(() => ctx.ellipse(1, 2, 3, 4, 5, 6, 7)); |
| ignoreException(() => ctx.ellipse(8, 9, 10, 11, 12, 13, 14, true)); |
| }, |
| () => { |
| ctx.fill(); |
| ctx.fill("evenodd"); |
| ctx.fill(path12); |
| ctx.fill(path34, "evenodd"); |
| }, |
| () => { |
| ctx.fillRect(1, 2, 3, 4); |
| }, |
| () => { |
| ctx.fillStyle; |
| ctx.fillStyle = "test"; |
| ctx.fillStyle = linearGradient; |
| ctx.fillStyle = radialGradient; |
| ctx.fillStyle = pattern; |
| }, |
| () => { |
| ctx.fillText("testA", 1, 2); |
| ctx.fillText("testB", 3, 4, 5); |
| }, |
| () => { |
| ctx.font; |
| ctx.font = "test"; |
| }, |
| () => { |
| ignoreException(() => ctx.getImageData(1, 2, 3, 4)); |
| }, |
| () => { |
| ctx.getLineDash(); |
| }, |
| () => { |
| ctx.getTransform(); |
| }, |
| () => { |
| ctx.globalAlpha; |
| ctx.globalAlpha = 0; |
| }, |
| () => { |
| ctx.globalCompositeOperation; |
| ctx.globalCompositeOperation = "test"; |
| }, |
| () => { |
| ctx.imageSmoothingEnabled; |
| ctx.imageSmoothingEnabled = true; |
| }, |
| () => { |
| ctx.imageSmoothingQuality; |
| ctx.imageSmoothingQuality = "low"; |
| }, |
| () => { |
| ctx.isPointInPath(path12, 5, 6); |
| ctx.isPointInPath(path34, 7, 8, "evenodd"); |
| ctx.isPointInPath(9, 10); |
| ctx.isPointInPath(11, 12, "evenodd"); |
| }, |
| () => { |
| ctx.isPointInStroke(path12, 3, 4); |
| ctx.isPointInStroke(5, 6); |
| }, |
| () => { |
| ctx.lineCap; |
| ctx.lineCap = "test"; |
| }, |
| () => { |
| ctx.lineDashOffset; |
| ctx.lineDashOffset = 1; |
| }, |
| () => { |
| ctx.lineJoin; |
| ctx.lineJoin = "test"; |
| }, |
| () => { |
| ctx.lineTo(1, 2); |
| }, |
| () => { |
| ctx.lineWidth; |
| ctx.lineWidth = 1; |
| }, |
| () => { |
| ctx.measureText("test"); |
| }, |
| () => { |
| ctx.miterLimit; |
| ctx.miterLimit = 1; |
| }, |
| () => { |
| ctx.moveTo(1, 2); |
| }, |
| () => { |
| ctx.putImageData(imageData14, 5, 6); |
| ctx.putImageData(imageData23, 7, 8, 9, 10, 11, 12); |
| }, |
| () => { |
| ctx.quadraticCurveTo(1, 2, 3, 4); |
| }, |
| () => { |
| ctx.rect(1, 2, 3, 4); |
| }, |
| () => { |
| ctx.resetTransform(); |
| }, |
| () => { |
| ctx.restore(); |
| --saveCount; |
| }, |
| () => { |
| ctx.rotate(1); |
| }, |
| () => { |
| ctx.save(); |
| ++saveCount; |
| }, |
| () => { |
| ctx.scale(1, 2); |
| }, |
| () => { |
| ctx.setAlpha(); |
| ctx.setAlpha(1); |
| }, |
| () => { |
| ctx.setCompositeOperation(); |
| ctx.setCompositeOperation("test"); |
| }, |
| () => { |
| ctx.setFillColor("testA"); |
| ctx.setFillColor("testB", 1); |
| ctx.setFillColor(2); |
| ctx.setFillColor(3, 4); |
| ctx.setFillColor(5, 6, 7, 8); |
| ctx.setFillColor(9, 10, 11, 12, 13); |
| }, |
| () => { |
| ctx.setLineCap(); |
| ctx.setLineCap("test"); |
| }, |
| () => { |
| ctx.setLineDash([1, 2]); |
| }, |
| () => { |
| ctx.setLineJoin(); |
| ctx.setLineJoin("test"); |
| }, |
| () => { |
| ctx.setLineWidth(); |
| ctx.setLineWidth(1); |
| }, |
| () => { |
| ctx.setMiterLimit(); |
| ctx.setMiterLimit(1); |
| }, |
| () => { |
| ctx.setShadow(1, 2, 3); |
| ctx.setShadow(4, 5, 6, "test", 7); |
| ctx.setShadow(8, 9, 10, 11); |
| ctx.setShadow(12, 13, 14, 15, 16); |
| ctx.setShadow(17, 18, 19, 20, 21, 22, 23); |
| ctx.setShadow(24, 25, 26, 27, 28, 29, 30, 31); |
| }, |
| () => { |
| ctx.setStrokeColor("testA"); |
| ctx.setStrokeColor("testB", 1); |
| ctx.setStrokeColor(2); |
| ctx.setStrokeColor(3, 4); |
| ctx.setStrokeColor(5, 6, 7, 8); |
| ctx.setStrokeColor(9, 10, 11, 12, 13); |
| }, |
| () => { |
| ctx.setTransform(1, 2, 3, 4, 5, 6); |
| ignoreException(() => ctx.setTransform()); |
| ignoreException(() => ctx.setTransform(new DOMMatrix([7, 8, 9, 10, 11, 12]))); |
| }, |
| () => { |
| ctx.shadowBlur; |
| ctx.shadowBlur = 1; |
| }, |
| () => { |
| ctx.shadowColor; |
| ctx.shadowColor = "test"; |
| }, |
| () => { |
| ctx.shadowOffsetX; |
| ctx.shadowOffsetX = 1; |
| }, |
| () => { |
| ctx.shadowOffsetY; |
| ctx.shadowOffsetY = 1; |
| }, |
| () => { |
| ctx.stroke(); |
| ctx.stroke(path12); |
| }, |
| () => { |
| ctx.strokeRect(1, 2, 3, 4); |
| }, |
| () => { |
| ctx.strokeStyle; |
| ctx.strokeStyle = "test"; |
| ctx.strokeStyle = linearGradient; |
| ctx.strokeStyle = radialGradient; |
| ctx.strokeStyle = pattern; |
| }, |
| () => { |
| ctx.strokeText("testA", 1, 2); |
| ctx.strokeText("testB", 3, 4, 5); |
| }, |
| () => { |
| ctx.textAlign; |
| ctx.textAlign = "test"; |
| }, |
| () => { |
| ctx.textBaseline; |
| ctx.textBaseline = "test"; |
| }, |
| () => { |
| ctx.transform(1, 2, 3, 4, 5, 6); |
| }, |
| () => { |
| ctx.translate(1, 2); |
| }, |
| () => { |
| ctx.webkitImageSmoothingEnabled; |
| ctx.webkitImageSmoothingEnabled = true; |
| }, |
| () => { |
| ctx.webkitLineDash; |
| ctx.webkitLineDash = [1, 2]; |
| }, |
| () => { |
| ctx.webkitLineDashOffset; |
| ctx.webkitLineDashOffset = 1; |
| }, |
| () => { |
| ctx.canvas.width; |
| ctx.canvas.width = 2; |
| }, |
| () => { |
| ctx.canvas.height; |
| ctx.canvas.height = 2; |
| }, |
| () => { |
| TestPage.dispatchEventToFrontend("LastFrame"); |
| }, |
| ]; |
| let index = 0; |
| function executeFrameFunction() { |
| frames[index++](); |
| if (index < frames.length) |
| requestAnimationFrameId = requestAnimationFrame(executeFrameFunction); |
| }; |
| executeFrameFunction(); |
| } |
| |
| function performConsoleActions() { |
| console.record(ctx, {name: "TEST"}); |
| |
| ctx.fill(); |
| |
| console.recordEnd(ctx); |
| |
| ctx.stroke(); |
| } |
| |
| function performSavePreActions() { |
| cancelActions(); |
| ctx.restore(); |
| ctx.restore(); |
| |
| function saveFillStyle(value) { |
| ctx.save(); |
| ctx.fillStyle = value; |
| } |
| |
| saveFillStyle("#ff0000"); |
| saveFillStyle("#00ff00"); |
| saveFillStyle("#0000ff"); |
| } |
| |
| function performSavePostActions() { |
| ctx.fill(); |
| } |
| |
| function performNaNActions() { |
| ctx.globalAlpha = NaN; |
| } |
| |
| function test() { |
| let suite = InspectorTest.createAsyncSuite("Canvas.recording2D"); |
| |
| suite.addTestCase({ |
| name: "Canvas.recording2D.singleFrame", |
| description: "Check that the recording is stopped after a single frame.", |
| test(resolve, reject) { |
| startRecording(WI.Canvas.ContextType.Canvas2D, resolve, reject, {frameCount: 1}); |
| }, |
| timeout: -1, |
| }); |
| |
| suite.addTestCase({ |
| name: "Canvas.recording2D.multipleFrames", |
| description: "Check that recording data is serialized correctly for multiple frames.", |
| test(resolve, reject) { |
| startRecording(WI.Canvas.ContextType.Canvas2D, resolve, reject); |
| }, |
| timeout: -1, |
| }); |
| |
| suite.addTestCase({ |
| name: "Canvas.recording2D.memoryLimit", |
| description: "Check that the recording is stopped when it reaches the memory limit.", |
| test(resolve, reject) { |
| startRecording(WI.Canvas.ContextType.Canvas2D, resolve, reject, {memoryLimit: 10}); |
| }, |
| timeout: -1, |
| }); |
| |
| suite.addTestCase({ |
| name: "Canvas.recording2D.Console", |
| description: "Check that a recording can be triggered by console.record().", |
| test(resolve, reject) { |
| consoleRecord(WI.Canvas.ContextType.Canvas2D, resolve, reject); |
| }, |
| }); |
| |
| suite.addTestCase({ |
| name: "Canvas.recording2D.ActionParameterNaN", |
| description: "Check that NaN is converted into the proper value for serialization.", |
| test(resolve, reject) { |
| let canvas = getCanvas(WI.Canvas.ContextType.Canvas2D); |
| if (!canvas) { |
| reject("Missing 2D canvas."); |
| return; |
| } |
| |
| canvas.awaitEvent(WI.Canvas.Event.RecordingStopped) |
| .then((event) => { |
| let recording = event.data.recording.toJSON(); |
| |
| let frames = recording.frames; |
| InspectorTest.expectEqual(frames.length, 1, "The recording should have 1 frame."); |
| |
| let actions = frames[0].actions; |
| InspectorTest.expectEqual(actions.length, 1, "The first frame should have 1 action."); |
| InspectorTest.expectEqual(actions[0][1].length, 1, "The action should have 1 parameter."); |
| InspectorTest.expectEqual(actions[0][1][0], null, "The parameter should be null."); |
| }) |
| .then(resolve, reject); |
| |
| canvas.awaitEvent(WI.Canvas.Event.RecordingStarted) |
| .then((event) => { |
| InspectorTest.evaluateInPage(`performNaNActions()`); |
| }); |
| |
| const frameCount = 1; |
| CanvasAgent.startRecording(canvas.identifier, frameCount) |
| .catch(reject); |
| }, |
| }); |
| |
| suite.addTestCase({ |
| name: "Canvas.recording2D.ExistingSaves", |
| description: "Check that existing save calls are sent to the frontend.", |
| test(resolve, reject) { |
| let canvas = getCanvas(WI.Canvas.ContextType.Canvas2D); |
| if (!canvas) { |
| reject("Missing 2D canvas."); |
| return; |
| } |
| |
| async function logStates(recording) { |
| async function compare(index, expected) { |
| let state = await WI.RecordingState.swizzleInitialState(recording, recording.initialState.states[index]); |
| InspectorTest.expectEqual(state.get("fillStyle"), expected, `State ${index} should match expected fillStyle value.`) |
| } |
| |
| await compare(0, "#000000"); |
| await compare(1, "#ff0000"); |
| await compare(2, "#00ff00"); |
| await compare(3, "#0000ff"); |
| } |
| |
| canvas.awaitEvent(WI.Canvas.Event.RecordingStopped) |
| .then((event) => { |
| let {recording} = event.data; |
| |
| InspectorTest.expectEqual(recording.initialState.states.length, 4, "There should be 4 existing states."); |
| |
| logStates(recording) |
| .then(resolve, reject); |
| }); |
| |
| canvas.awaitEvent(WI.Canvas.Event.RecordingStarted) |
| .then((event) => { |
| InspectorTest.evaluateInPage(`performSavePostActions()`).catch(reject); |
| }); |
| |
| InspectorTest.evaluateInPage(`performSavePreActions()`) |
| .then(() => { |
| const frameCount = 1; |
| CanvasAgent.startRecording(canvas.identifier, frameCount).catch(reject); |
| }, reject); |
| }, |
| }); |
| |
| suite.addTestCase({ |
| name: "Canvas.recording2D.NoActions", |
| description: "Check that a canvas is still able to be recorded after stopping a recording with no actions.", |
| test(resolve, reject) { |
| let canvas = getCanvas(WI.Canvas.ContextType.Canvas2D); |
| if (!canvas) { |
| reject("Missing 2D canvas."); |
| return; |
| } |
| |
| let eventCount = 0; |
| function handleRecordingStopped(event) { |
| InspectorTest.assert(!event.data.recording, "The recording payload should be null."); |
| |
| ++eventCount; |
| if (eventCount == 1) { |
| InspectorTest.pass("A recording should have been started and stopped once."); |
| |
| canvas.startRecording(); |
| canvas.stopRecording(); |
| } else if (eventCount >= 2) { |
| InspectorTest.pass("A recording should have been started and stopped twice."); |
| |
| canvas.removeEventListener(WI.Canvas.Event.RecordingStopped, handleRecordingStopped); |
| resolve(); |
| } |
| } |
| canvas.addEventListener(WI.Canvas.Event.RecordingStopped, handleRecordingStopped); |
| |
| canvas.startRecording(); |
| canvas.stopRecording(); |
| }, |
| }); |
| |
| suite.runTestCasesAndFinish(); |
| } |
| </script> |
| </head> |
| <body onload="load()"> |
| <p>Test that CanvasManager is able to record actions made to 2D canvas contexts.</p> |
| </body> |
| </html> |