Source/JavaScriptCore/ChangeLog

 12018-10-12 Devin Rousso <drousso@apple.com>
 2
 3 Web Inspector: Canvas: capture previously saved states and add them to the recording payload
 4 https://bugs.webkit.org/show_bug.cgi?id=190473
 5
 6 Reviewed by NOBODY (OOPS!).
 7
 8 * inspector/protocol/Recording.json:
 9 Add `states` key to `InitialState` object.
 10
1112018-10-11 Guillaume Emont <guijemont@igalia.com>
212
313 [JSC] Remove gcc warnings on mips and armv7

Source/WebCore/ChangeLog

 12018-10-12 Devin Rousso <drousso@apple.com>
 2
 3 Web Inspector: Canvas: capture previously saved states and add them to the recording payload
 4 https://bugs.webkit.org/show_bug.cgi?id=190473
 5
 6 Reviewed by NOBODY (OOPS!).
 7
 8 Updated existing tests: inspector/canvas/recording-2d.html
 9 inspector/model/recording.html
 10
 11 Instead of sending a single object of the current state of the context, send an array of
 12 objects, one for each restore point.
 13
 14 * html/canvas/CanvasRenderingContext2DBase.h:
 15 * html/canvas/CanvasRenderingContext2DBase.cpp:
 16 (WebCore::CanvasRenderingContext2DBase::stateStack): Added.
 17
 18 * inspector/InspectorCanvas.h:
 19 * inspector/InspectorCanvas.cpp:
 20 (WebCore::InspectorCanvas::indexForKey): Added.
 21 (WebCore::InspectorCanvas::buildInitialState):
 22
1232018-10-11 Don Olmstead <don.olmstead@sony.com>
224
325 Add Houdini specs to features.json

Source/WebInspectorUI/ChangeLog

 12018-10-12 Devin Rousso <drousso@apple.com>
 2
 3 Web Inspector: Canvas: capture previously saved states and add them to the recording payload
 4 https://bugs.webkit.org/show_bug.cgi?id=190473
 5
 6 Reviewed by NOBODY (OOPS!).
 7
 8 Instead of sending a single object of the current state of the context, send an array of
 9 objects, one for each restore point. When replaying, recreate each restore point before
 10 applying the selected action(s).
 11
 12 * UserInterface/Models/Recording.js:
 13 (WI.Recording):
 14 (WI.Recording.fromPayload):
 15 (WI.Recording.prototype.toJSON):
 16 (WI.Recording.prototype.async._process):
 17 (WI.Recording.prototype.async._swizzleState): Added.
 18 * UserInterface/Models/RecordingAction.js:
 19 (WI.RecordingAction):
 20 (WI.RecordingAction.deriveCurrentState): Added.
 21 (WI.RecordingAction.prototype.get states): Added.
 22 (WI.RecordingAction.prototype.process):
 23 (WI.RecordingAction.prototype.get state): Deleted.
 24
 25 * UserInterface/Views/RecordingContentView.js:
 26 (WI.RecordingContentView.prototype._generateContentCanvas2D):
 27
 28 * UserInterface/Views/RecordingStateDetailsSidebarPanel.js:
 29 (WI.RecordingStateDetailsSidebarPanel.prototype._generateDetailsCanvas2D):
 30 Default to showing the most recent (current) state.
 31
 32 * UserInterface/Views/CanvasTabContentView.js:
 33 (WI.CanvasTabContentView.prototype.initialLayout): Added.
 34 (WI.CanvasTabContentView.prototype._addCanvas):
 35 (WI.CanvasTabContentView.prototype._removeCanvas):
 36 (WI.CanvasTabContentView.prototype._addRecording): Added.
 37 (WI.CanvasTabContentView.prototype._recordingImportedOrStopped):
 38 (WI.CanvasTabContentView.prototype._recordingAdded): Deleted.
 39 * UserInterface/Controllers/CanvasManager.js:
 40 (WI.CanvasManager):
 41 (WI.CanvasManager.prototype.get importedRecordings): Added.
 42 (WI.CanvasManager.prototype.importRecording):
 43 Drive-by: store imported recordings on `WI.CanvasManager` so that if the Canvas tab is
 44 closed we can still show the list of imported recordings.
 45
1462018-10-11 Devin Rousso <drousso@apple.com>
247
348 Web Inspector: Network: detail view reverts to "Response" when new requests are added

Source/JavaScriptCore/inspector/protocol/Recording.json

1919 "description": "Information about the initial state of the recorded object.",
2020 "properties": [
2121 { "name": "attributes", "type": "object", "optional": true, "description": "Key-value map for each attribute of the state." },
 22 { "name": "states", "type": "array", "items": { "type": "object" }, "optional": true, "description": "Array of saved states of the context." },
2223 { "name": "parameters", "type": "array", "items": { "type": "any" }, "optional": true, "description": "Array of values that were used to construct the recorded object." },
2324 { "name": "content", "type": "string", "optional": true, "description": "Current content at the start of the recording." }
2425 ]

Source/WebCore/html/canvas/CanvasRenderingContext2DBase.cpp

@@String CanvasRenderingContext2DBase::replayDisplayListAsText(DisplayList::AsText
20632063 return displayList->asText(flags);
20642064}
20652065
 2066const Vector<CanvasRenderingContext2DBase::State, 1>& CanvasRenderingContext2DBase::stateStack()
 2067{
 2068 realizeSaves();
 2069 return m_stateStack;
 2070}
 2071
20662072void CanvasRenderingContext2DBase::paintRenderingResultsToCanvas()
20672073{
20682074 if (UNLIKELY(m_usesDisplayListDrawing)) {

Source/WebCore/html/canvas/CanvasRenderingContext2DBase.h

@@public:
274274 };
275275
276276 const State& state() const { return m_stateStack.last(); }
 277 const Vector<State, 1>& stateStack();
277278
278279protected:
279280 static const int DefaultFontSize;

Source/WebCore/inspector/InspectorCanvas.cpp

@@int InspectorCanvas::indexForData(DuplicateDataVariant data)
406406 return static_cast<int>(index);
407407}
408408
 409String InspectorCanvas::indexForKey(const String& key)
 410{
 411 return String::number(indexForData(key));
 412}
 413
409414static Ref<JSON::ArrayOf<double>> buildArrayForAffineTransform(const AffineTransform& affineTransform)
410415{
411416 auto array = JSON::ArrayOf<double>::create();

@@template<typename T> static Ref<JSON::ArrayOf<JSON::Value>> buildArrayForVector(
428433
429434Ref<Inspector::Protocol::Recording::InitialState> InspectorCanvas::buildInitialState()
430435{
431  auto initialState = Inspector::Protocol::Recording::InitialState::create().release();
 436 auto initialStatePayload = Inspector::Protocol::Recording::InitialState::create().release();
432437
433  auto attributes = JSON::Object::create();
434  attributes->setInteger("width"_s, m_context.canvasBase().width());
435  attributes->setInteger("height"_s, m_context.canvasBase().height());
 438 auto attributesPayload = JSON::Object::create();
 439 attributesPayload->setInteger("width"_s, m_context.canvasBase().width());
 440 attributesPayload->setInteger("height"_s, m_context.canvasBase().height());
436441
437  auto parameters = JSON::ArrayOf<JSON::Value>::create();
 442 auto statesPayload = JSON::ArrayOf<JSON::Object>::create();
 443
 444 auto parametersPayload = JSON::ArrayOf<JSON::Value>::create();
438445
439446 if (is<CanvasRenderingContext2D>(m_context)) {
440447 auto& context2d = downcast<CanvasRenderingContext2D>(m_context);
441  auto& state = context2d.state();
442 
443  attributes->setArray("setTransform"_s, buildArrayForAffineTransform(state.transform));
444  attributes->setDouble("globalAlpha"_s, context2d.globalAlpha());
445  attributes->setInteger("globalCompositeOperation"_s, indexForData(context2d.globalCompositeOperation()));
446  attributes->setDouble("lineWidth"_s, context2d.lineWidth());
447  attributes->setInteger("lineCap"_s, indexForData(convertEnumerationToString(context2d.lineCap())));
448  attributes->setInteger("lineJoin"_s, indexForData(convertEnumerationToString(context2d.lineJoin())));
449  attributes->setDouble("miterLimit"_s, context2d.miterLimit());
450  attributes->setDouble("shadowOffsetX"_s, context2d.shadowOffsetX());
451  attributes->setDouble("shadowOffsetY"_s, context2d.shadowOffsetY());
452  attributes->setDouble("shadowBlur"_s, context2d.shadowBlur());
453  attributes->setInteger("shadowColor"_s, indexForData(context2d.shadowColor()));
454 
455  // The parameter to `setLineDash` is itself an array, so we need to wrap the parameters
456  // list in an array to allow spreading.
457  auto setLineDash = JSON::ArrayOf<JSON::Value>::create();
458  setLineDash->addItem(buildArrayForVector(state.lineDash));
459  attributes->setArray("setLineDash"_s, WTFMove(setLineDash));
460 
461  attributes->setDouble("lineDashOffset"_s, context2d.lineDashOffset());
462  attributes->setInteger("font"_s, indexForData(context2d.font()));
463  attributes->setInteger("textAlign"_s, indexForData(convertEnumerationToString(context2d.textAlign())));
464  attributes->setInteger("textBaseline"_s, indexForData(convertEnumerationToString(context2d.textBaseline())));
465  attributes->setInteger("direction"_s, indexForData(convertEnumerationToString(context2d.direction())));
466 
467  int strokeStyleIndex;
468  if (auto canvasGradient = state.strokeStyle.canvasGradient())
469  strokeStyleIndex = indexForData(canvasGradient.get());
470  else if (auto canvasPattern = state.strokeStyle.canvasPattern())
471  strokeStyleIndex = indexForData(canvasPattern.get());
472  else
473  strokeStyleIndex = indexForData(state.strokeStyle.color());
474  attributes->setInteger("strokeStyle"_s, strokeStyleIndex);
475 
476  int fillStyleIndex;
477  if (auto canvasGradient = state.fillStyle.canvasGradient())
478  fillStyleIndex = indexForData(canvasGradient.get());
479  else if (auto canvasPattern = state.fillStyle.canvasPattern())
480  fillStyleIndex = indexForData(canvasPattern.get());
481  else
482  fillStyleIndex = indexForData(state.fillStyle.color());
483  attributes->setInteger("fillStyle"_s, fillStyleIndex);
484 
485  attributes->setBoolean("imageSmoothingEnabled"_s, context2d.imageSmoothingEnabled());
486  attributes->setInteger("imageSmoothingQuality"_s, indexForData(convertEnumerationToString(context2d.imageSmoothingQuality())));
487 
488  auto setPath = JSON::ArrayOf<JSON::Value>::create();
489  setPath->addItem(indexForData(buildStringFromPath(context2d.getPath()->path())));
490  attributes->setArray("setPath"_s, WTFMove(setPath));
 448 for (auto& state : context2d.stateStack()) {
 449 RefPtr<JSON::Object> statePayload = JSON::Object::create();
 450
 451 statePayload->setArray(indexForKey("setTransform"_s), buildArrayForAffineTransform(state.transform));
 452 statePayload->setDouble(indexForKey("globalAlpha"_s), context2d.globalAlpha());
 453 statePayload->setInteger(indexForKey("globalCompositeOperation"_s), indexForData(context2d.globalCompositeOperation()));
 454 statePayload->setDouble(indexForKey("lineWidth"_s), context2d.lineWidth());
 455 statePayload->setInteger(indexForKey("lineCap"_s), indexForData(convertEnumerationToString(context2d.lineCap())));
 456 statePayload->setInteger(indexForKey("lineJoin"_s), indexForData(convertEnumerationToString(context2d.lineJoin())));
 457 statePayload->setDouble(indexForKey("miterLimit"_s), context2d.miterLimit());
 458 statePayload->setDouble(indexForKey("shadowOffsetX"_s), context2d.shadowOffsetX());
 459 statePayload->setDouble(indexForKey("shadowOffsetY"_s), context2d.shadowOffsetY());
 460 statePayload->setDouble(indexForKey("shadowBlur"_s), context2d.shadowBlur());
 461 statePayload->setInteger(indexForKey("shadowColor"_s), indexForData(context2d.shadowColor()));
 462
 463 // The parameter to `setLineDash` is itself an array, so we need to wrap the parameters
 464 // list in an array to allow spreading.
 465 auto setLineDash = JSON::ArrayOf<JSON::Value>::create();
 466 setLineDash->addItem(buildArrayForVector(state.lineDash));
 467 statePayload->setArray(indexForKey("setLineDash"_s), WTFMove(setLineDash));
 468
 469 statePayload->setDouble(indexForKey("lineDashOffset"_s), context2d.lineDashOffset());
 470 statePayload->setInteger(indexForKey("font"_s), indexForData(context2d.font()));
 471 statePayload->setInteger(indexForKey("textAlign"_s), indexForData(convertEnumerationToString(context2d.textAlign())));
 472 statePayload->setInteger(indexForKey("textBaseline"_s), indexForData(convertEnumerationToString(context2d.textBaseline())));
 473 statePayload->setInteger(indexForKey("direction"_s), indexForData(convertEnumerationToString(context2d.direction())));
 474
 475 int strokeStyleIndex;
 476 if (auto canvasGradient = state.strokeStyle.canvasGradient())
 477 strokeStyleIndex = indexForData(canvasGradient.get());
 478 else if (auto canvasPattern = state.strokeStyle.canvasPattern())
 479 strokeStyleIndex = indexForData(canvasPattern.get());
 480 else
 481 strokeStyleIndex = indexForData(state.strokeStyle.color());
 482 statePayload->setInteger(indexForKey("strokeStyle"_s), strokeStyleIndex);
 483
 484 int fillStyleIndex;
 485 if (auto canvasGradient = state.fillStyle.canvasGradient())
 486 fillStyleIndex = indexForData(canvasGradient.get());
 487 else if (auto canvasPattern = state.fillStyle.canvasPattern())
 488 fillStyleIndex = indexForData(canvasPattern.get());
 489 else
 490 fillStyleIndex = indexForData(state.fillStyle.color());
 491 statePayload->setInteger(indexForKey("fillStyle"_s), fillStyleIndex);
 492
 493 statePayload->setBoolean(indexForKey("imageSmoothingEnabled"_s), context2d.imageSmoothingEnabled());
 494 statePayload->setInteger(indexForKey("imageSmoothingQuality"_s), indexForData(convertEnumerationToString(context2d.imageSmoothingQuality())));
 495
 496 auto setPath = JSON::ArrayOf<JSON::Value>::create();
 497 setPath->addItem(indexForData(buildStringFromPath(context2d.getPath()->path())));
 498 statePayload->setArray(indexForKey("setPath"_s), WTFMove(setPath));
 499
 500 statesPayload->addItem(WTFMove(statePayload));
 501 }
491502 }
492503#if ENABLE(WEBGL)
493504 else if (is<WebGLRenderingContextBase>(m_context)) {
494505 WebGLRenderingContextBase& contextWebGLBase = downcast<WebGLRenderingContextBase>(m_context);
495  if (std::optional<WebGLContextAttributes> attributes = contextWebGLBase.getContextAttributes()) {
496  RefPtr<JSON::Object> contextAttributes = JSON::Object::create();
497  contextAttributes->setBoolean("alpha"_s, attributes->alpha);
498  contextAttributes->setBoolean("depth"_s, attributes->depth);
499  contextAttributes->setBoolean("stencil"_s, attributes->stencil);
500  contextAttributes->setBoolean("antialias"_s, attributes->antialias);
501  contextAttributes->setBoolean("premultipliedAlpha"_s, attributes->premultipliedAlpha);
502  contextAttributes->setBoolean("preserveDrawingBuffer"_s, attributes->preserveDrawingBuffer);
503  contextAttributes->setBoolean("failIfMajorPerformanceCaveat"_s, attributes->failIfMajorPerformanceCaveat);
504  parameters->addItem(WTFMove(contextAttributes));
 506 if (std::optional<WebGLContextAttributes> webGLContextAttributes = contextWebGLBase.getContextAttributes()) {
 507 RefPtr<JSON::Object> webGLContextAttributesPayload = JSON::Object::create();
 508 webGLContextAttributesPayload->setBoolean("alpha"_s, webGLContextAttributes->alpha);
 509 webGLContextAttributesPayload->setBoolean("depth"_s, webGLContextAttributes->depth);
 510 webGLContextAttributesPayload->setBoolean("stencil"_s, webGLContextAttributes->stencil);
 511 webGLContextAttributesPayload->setBoolean("antialias"_s, webGLContextAttributes->antialias);
 512 webGLContextAttributesPayload->setBoolean("premultipliedAlpha"_s, webGLContextAttributes->premultipliedAlpha);
 513 webGLContextAttributesPayload->setBoolean("preserveDrawingBuffer"_s, webGLContextAttributes->preserveDrawingBuffer);
 514 webGLContextAttributesPayload->setBoolean("failIfMajorPerformanceCaveat"_s, webGLContextAttributes->failIfMajorPerformanceCaveat);
 515 parametersPayload->addItem(WTFMove(webGLContextAttributesPayload));
505516 }
506517 }
507518#endif
508519
509  initialState->setAttributes(WTFMove(attributes));
 520 initialStatePayload->setAttributes(WTFMove(attributesPayload));
 521
 522 if (statesPayload->length())
 523 initialStatePayload->setStates(WTFMove(statesPayload));
510524
511  if (parameters->length())
512  initialState->setParameters(WTFMove(parameters));
 525 if (parametersPayload->length())
 526 initialStatePayload->setParameters(WTFMove(parametersPayload));
513527
514  initialState->setContent(getCanvasContentAsDataURL());
 528 initialStatePayload->setContent(getCanvasContentAsDataURL());
515529
516  return initialState;
 530 return initialStatePayload;
517531}
518532
519533Ref<JSON::ArrayOf<JSON::Value>> InspectorCanvas::buildAction(const String& name, Vector<RecordCanvasActionVariant>&& parameters)

Source/WebCore/inspector/InspectorCanvas.h

@@private:
9696 >;
9797
9898 int indexForData(DuplicateDataVariant);
 99 String indexForKey(const String&);
99100 Ref<Inspector::Protocol::Recording::InitialState> buildInitialState();
100101 Ref<JSON::ArrayOf<JSON::Value>> buildAction(const String&, Vector<RecordCanvasActionVariant>&& = { });
101102 Ref<JSON::ArrayOf<JSON::Value>> buildArrayForCanvasGradient(const CanvasGradient&);

Source/WebInspectorUI/UserInterface/Controllers/CanvasManager.js

@@WI.CanvasManager = class CanvasManager extends WI.Object
3333
3434 this._canvasIdentifierMap = new Map;
3535 this._shaderProgramIdentifierMap = new Map;
 36 this._importedRecordings = new Set;
3637
3738 if (window.CanvasAgent)
3839 CanvasAgent.enable();

@@WI.CanvasManager = class CanvasManager extends WI.Object
4041
4142 // Public
4243
 44 get importedRecordings() { return this._importedRecordings; }
 45
4346 get canvases()
4447 {
4548 return [...this._canvasIdentifierMap.values()];

@@WI.CanvasManager = class CanvasManager extends WI.Object
7578 filename = filename.substring(0, extensionStart);
7679 recording.createDisplayName(filename);
7780
 81 this._importedRecordings.add(recording);
 82
7883 this.dispatchEventToListeners(WI.CanvasManager.Event.RecordingImported, {recording});
7984 });
8085 }

Source/WebInspectorUI/UserInterface/Models/Recording.js

@@WI.Recording = class Recording extends WI.Object
4242 this._source = null;
4343
4444 this._processContext = null;
 45 this._processStates = [];
4546 this._processing = false;
4647 }
4748

@@WI.Recording = class Recording extends WI.Object
7374 payload.initialState = {};
7475 if (typeof payload.initialState.attributes !== "object" || payload.initialState.attributes === null)
7576 payload.initialState.attributes = {};
 77 if (!Array.isArray(payload.initialState.states) || payload.initialState.states.some((item) => typeof item !== "object" || item === null)){
 78 payload.initialState.states = [];
 79
 80 // COMPATIBILITY (iOS 12.1): Recording.types.InitialState.states did not exist yet
 81 if (!isEmptyObject(payload.initialState.attributes)) {
 82 let {width, height, ...state} = payload.initialState.attributes;
 83 if (!isEmptyObject(state))
 84 payload.initialState.states.push(state);
 85 }
 86 }
7687 if (!Array.isArray(payload.initialState.parameters))
7788 payload.initialState.parameters = [];
7889 if (typeof payload.initialState.content !== "string")

@@WI.Recording = class Recording extends WI.Object
337348 let initialState = {};
338349 if (!isEmptyObject(this._initialState.attributes))
339350 initialState.attributes = this._initialState.attributes;
 351 if (this._initialState.states.length)
 352 initialState.states = this._initialState.states;
340353 if (this._initialState.parameters.length)
341354 initialState.parameters = this._initialState.parameters;
342355 if (this._initialState.content && this._initialState.content.length)

@@WI.Recording = class Recording extends WI.Object
362375 let initialContent = await WI.ImageUtilities.promisifyLoad(this._initialState.content);
363376 this._processContext.drawImage(initialContent, 0, 0);
364377
365  for (let [key, value] of Object.entries(this._initialState.attributes)) {
366  switch (key) {
367  case "setTransform":
368  value = [await this.swizzle(value, WI.Recording.Swizzle.DOMMatrix)];
369  break;
370 
371  case "fillStyle":
372  case "strokeStyle":
373  let [gradient, pattern, string] = await Promise.all([
374  this.swizzle(value, WI.Recording.Swizzle.CanvasGradient),
375  this.swizzle(value, WI.Recording.Swizzle.CanvasPattern),
376  this.swizzle(value, WI.Recording.Swizzle.String),
377  ]);
378  if (gradient && !pattern)
379  value = gradient;
380  else if (pattern && !gradient)
381  value = pattern;
 378 for (let state of this._initialState.states) {
 379 let swizzledState = await this._swizzleState(state);
 380 for (let [key, value] of Object.entries(swizzledState)) {
 381 try {
 382 if (WI.RecordingAction.isFunctionForType(this._type, key))
 383 this._processContext[key](...value);
382384 else
383  value = string;
384  break;
385 
386  case "direction":
387  case "font":
388  case "globalCompositeOperation":
389  case "imageSmoothingEnabled":
390  case "imageSmoothingQuality":
391  case "lineCap":
392  case "lineJoin":
393  case "shadowColor":
394  case "textAlign":
395  case "textBaseline":
396  value = await this.swizzle(value, WI.Recording.Swizzle.String);
397  break;
398 
399  case "setPath":
400  value = [await this.swizzle(value[0], WI.Recording.Swizzle.Path2D)];
401  break;
 385 this._processContext[key] = value;
 386 } catch { }
402387 }
403388
404  if (value === undefined || (Array.isArray(value) && value.includes(undefined)))
405  continue;
406 
407  try {
408  if (WI.RecordingAction.isFunctionForType(this._type, key))
409  this._processContext[key](...value);
410  else
411  this._processContext[key] = value;
412  } catch { }
 389 // The last state represents the current state, which should not be saved.
 390 if (state !== this._initialState.states.lastValue) {
 391 this._processContext.save();
 392 this._processStates.push(WI.RecordingAction.deriveCurrentState(this._type, this._processContext));
 393 }
413394 }
414395 }
415396 }

@@WI.Recording = class Recording extends WI.Object
417398 // The first action is always a WI.RecordingInitialStateAction, which doesn't need to swizzle().
418399 // Since it is not associated with a WI.RecordingFrame, it has to manually process().
419400 if (!this._actions[0].ready) {
420  this._actions[0].process(this, this._processContext);
 401 this._actions[0].process(this, this._processContext, this._processStates);
421402 this.dispatchEventToListeners(WI.Recording.Event.ProcessedAction, {action: this._actions[0], index: 0});
422403 }
423404

@@WI.Recording = class Recording extends WI.Object
425406 let startTime = Date.now();
426407
427408 let cumulativeActionIndex = 0;
 409 let lastAction = this._actions[cumulativeActionIndex];
428410 for (let frameIndex = 0; frameIndex < this._frames.length; ++frameIndex) {
429411 let frame = this._frames[frameIndex];
430412
431413 if (frame.actions.lastValue.ready) {
432414 cumulativeActionIndex += frame.actions.length;
 415 lastAction = frame.actions.lastValue;
433416 continue;
434417 }
435418

@@WI.Recording = class Recording extends WI.Object
437420 ++cumulativeActionIndex;
438421
439422 let action = frame.actions[actionIndex];
440  if (action.ready)
 423 if (action.ready) {
 424 lastAction = action;
441425 continue;
 426 }
442427
443428 await action.swizzle(this);
444429
445  action.process(this, this._processContext);
 430 action.process(this, this._processContext, this._processStates, {lastAction});
446431
447432 if (action.isVisual)
448433 this._visualActionIndexes.push(cumulativeActionIndex);

@@WI.Recording = class Recording extends WI.Object
458443 startTime = Date.now();
459444 }
460445
 446 lastAction = action;
 447
461448 if (!this._processing)
462449 return;
463450 }

@@WI.Recording = class Recording extends WI.Object
469456 this._processContext = null;
470457 this._processing = false;
471458 }
 459
 460 async _swizzleState(state)
 461 {
 462 let swizzledState = {};
 463
 464 for (let [key, value] of Object.entries(state)) {
 465 // COMPATIBILITY (iOS 12.1): Recording.types.InitialState.states did not exist yet
 466 let keyIndex = parseInt(key);
 467 if (!isNaN(keyIndex))
 468 key = await this.swizzle(keyIndex, WI.Recording.Swizzle.String);
 469
 470 switch (key) {
 471 case "setTransform":
 472 value = [await this.swizzle(value, WI.Recording.Swizzle.DOMMatrix)];
 473 break;
 474
 475 case "fillStyle":
 476 case "strokeStyle":
 477 let [gradient, pattern, string] = await Promise.all([
 478 this.swizzle(value, WI.Recording.Swizzle.CanvasGradient),
 479 this.swizzle(value, WI.Recording.Swizzle.CanvasPattern),
 480 this.swizzle(value, WI.Recording.Swizzle.String),
 481 ]);
 482 if (gradient && !pattern)
 483 value = gradient;
 484 else if (pattern && !gradient)
 485 value = pattern;
 486 else
 487 value = string;
 488 break;
 489
 490 case "direction":
 491 case "font":
 492 case "globalCompositeOperation":
 493 case "imageSmoothingQuality":
 494 case "lineCap":
 495 case "lineJoin":
 496 case "shadowColor":
 497 case "textAlign":
 498 case "textBaseline":
 499 value = await this.swizzle(value, WI.Recording.Swizzle.String);
 500 break;
 501
 502 case "globalAlpha":
 503 case "lineWidth":
 504 case "miterLimit":
 505 case "shadowOffsetX":
 506 case "shadowOffsetY":
 507 case "shadowBlur":
 508 case "lineDashOffset":
 509 value = await this.swizzle(value, WI.Recording.Swizzle.Number);
 510 break;
 511
 512 case "setPath":
 513 value = [await this.swizzle(value[0], WI.Recording.Swizzle.Path2D)];
 514 break;
 515 }
 516
 517 if (value === undefined || (Array.isArray(value) && value.includes(undefined)))
 518 continue;
 519
 520 swizzledState[key] = value;
 521 }
 522
 523 return swizzledState;
 524 }
472525};
473526
474527WI.Recording.Event = {

Source/WebInspectorUI/UserInterface/Models/RecordingAction.js

@@WI.RecordingAction = class RecordingAction extends WI.Object
4646 this._isVisual = false;
4747 this._hasVisibleEffect = undefined;
4848
49  this._state = null;
 49 this._states = [];
5050 this._stateModifiers = new Set;
5151
5252 this._swizzled = false;

@@WI.RecordingAction = class RecordingAction extends WI.Object
130130 return null;
131131 }
132132
 133 static deriveCurrentState(type, context)
 134 {
 135 if (type === WI.Recording.Type.Canvas2D) {
 136 let matrix = context.getTransform();
 137
 138 let state = {
 139 currentX: context.currentX,
 140 currentY: context.currentY,
 141 direction: context.direction,
 142 fillStyle: context.fillStyle,
 143 font: context.font,
 144 globalAlpha: context.globalAlpha,
 145 globalCompositeOperation: context.globalCompositeOperation,
 146 imageSmoothingEnabled: context.imageSmoothingEnabled,
 147 imageSmoothingQuality: context.imageSmoothingQuality,
 148 lineCap: context.lineCap,
 149 lineDash: context.getLineDash(),
 150 lineDashOffset: context.lineDashOffset,
 151 lineJoin: context.lineJoin,
 152 lineWidth: context.lineWidth,
 153 miterLimit: context.miterLimit,
 154 shadowBlur: context.shadowBlur,
 155 shadowColor: context.shadowColor,
 156 shadowOffsetX: context.shadowOffsetX,
 157 shadowOffsetY: context.shadowOffsetY,
 158 strokeStyle: context.strokeStyle,
 159 textAlign: context.textAlign,
 160 textBaseline: context.textBaseline,
 161 transform: [matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f],
 162 webkitImageSmoothingEnabled: context.webkitImageSmoothingEnabled,
 163 webkitLineDash: context.webkitLineDash,
 164 webkitLineDashOffset: context.webkitLineDashOffset,
 165 };
 166
 167 if (WI.ImageUtilities.supportsCanvasPathDebugging())
 168 state.setPath = [context.getPath()];
 169
 170 return state;
 171 }
 172
 173 return null;
 174 }
 175
133176 static _prototypeForType(type)
134177 {
135178 if (type === WI.Recording.Type.Canvas2D)

@@WI.RecordingAction = class RecordingAction extends WI.Object
153196 get isGetter() { return this._isGetter; }
154197 get isVisual() { return this._isVisual; }
155198 get hasVisibleEffect() { return this._hasVisibleEffect; }
156  get state() { return this._state; }
 199 get states() { return this._states; }
157200 get stateModifiers() { return this._stateModifiers; }
158201
159202 get ready()

@@WI.RecordingAction = class RecordingAction extends WI.Object
161204 return this._swizzled && this._processed;
162205 }
163206
164  process(recording, context)
 207 process(recording, context, states, {lastAction} = {})
165208 {
166209 console.assert(this._swizzled, "You must swizzle() before you can process().");
167210 console.assert(!this._processed, "You should only process() once.");

@@WI.RecordingAction = class RecordingAction extends WI.Object
205248 this._hasVisibleEffect = !Array.shallowEqual(contentBefore, getContent());
206249
207250 if (recording.type === WI.Recording.Type.Canvas2D) {
208  let matrix = context.getTransform();
209 
210  this._state = {
211  currentX: context.currentX,
212  currentY: context.currentY,
213  direction: context.direction,
214  fillStyle: context.fillStyle,
215  font: context.font,
216  globalAlpha: context.globalAlpha,
217  globalCompositeOperation: context.globalCompositeOperation,
218  imageSmoothingEnabled: context.imageSmoothingEnabled,
219  imageSmoothingQuality: context.imageSmoothingQuality,
220  lineCap: context.lineCap,
221  lineDash: context.getLineDash(),
222  lineDashOffset: context.lineDashOffset,
223  lineJoin: context.lineJoin,
224  lineWidth: context.lineWidth,
225  miterLimit: context.miterLimit,
226  shadowBlur: context.shadowBlur,
227  shadowColor: context.shadowColor,
228  shadowOffsetX: context.shadowOffsetX,
229  shadowOffsetY: context.shadowOffsetY,
230  strokeStyle: context.strokeStyle,
231  textAlign: context.textAlign,
232  textBaseline: context.textBaseline,
233  transform: [matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f],
234  webkitImageSmoothingEnabled: context.webkitImageSmoothingEnabled,
235  webkitLineDash: context.webkitLineDash,
236  webkitLineDashOffset: context.webkitLineDashOffset,
237  };
238 
239  if (WI.ImageUtilities.supportsCanvasPathDebugging())
240  this._state.setPath = [context.getPath()];
 251 let currentState = WI.RecordingAction.deriveCurrentState(recording.type, context);
 252 console.assert(currentState);
 253
 254 if (this.name === "save")
 255 states.push(currentState);
 256 else if (this.name === "restore")
 257 states.pop();
 258
 259 this._states = states.slice();
 260 this._states.push(currentState);
 261
 262 if (lastAction) {
 263 let lastState = lastAction.states.lastValue;
 264 for (let key in currentState) {
 265 if (!(key in lastState) || (currentState[key] !== lastState[key] && !Object.shallowEqual(currentState[key], lastState[key])))
 266 this._stateModifiers.add(key);
 267 }
 268 }
241269 }
242270 }
243271
244  async swizzle(recording)
 272 async swizzle(recording, lastAction)
245273 {
246274 console.assert(!this._swizzled, "You should only swizzle() once.");
247275

Source/WebInspectorUI/UserInterface/Views/CanvasTabContentView.js

@@WI.CanvasTabContentView = class CanvasTabContentView extends WI.ContentBrowserTa
118118
119119 // Protected
120120
 121 initialLayout()
 122 {
 123 super.initialLayout();
 124
 125 const options = {
 126 suppressShowRecording: true,
 127 };
 128
 129 for (let recording of WI.canvasManager.importedRecordings)
 130 this._addRecording(recording, options);
 131 }
 132
121133 attached()
122134 {
123135 super.attached();

@@WI.CanvasTabContentView = class CanvasTabContentView extends WI.ContentBrowserTa
156168 this._canvasCollection.add(canvas);
157169
158170 for (let recording of canvas.recordingCollection)
159  this._recordingAdded(recording, {suppressShowRecording: true});
 171 this._addRecording(recording, {suppressShowRecording: true});
160172 }
161173
162174 _removeCanvas(canvas)

@@WI.CanvasTabContentView = class CanvasTabContentView extends WI.ContentBrowserTa
171183 };
172184
173185 for (let recording of canvas.recordingCollection)
174  this._recordingAdded(recording, options);
 186 this._addRecording(recording, options);
175187
176188 let currentContentView = this.contentBrowser.currentContentView;
177189 if (currentContentView instanceof WI.RecordingContentView && canvas.recordingCollection.has(currentContentView.representedObject))

@@WI.CanvasTabContentView = class CanvasTabContentView extends WI.ContentBrowserTa
182194 navigationSidebarPanel.updateRepresentedObjects();
183195 }
184196
 197 _addRecording(recording, options = {})
 198 {
 199 if (!recording.source) {
 200 const subtitle = null;
 201 let recordingTreeElement = new WI.GeneralTreeElement(["recording"], recording.displayName, subtitle, recording);
 202 this._importedRecordingsTreeElement.hidden = false;
 203 this._importedRecordingsTreeElement.appendChild(recordingTreeElement);
 204 }
 205
 206 if (!options.suppressShowRecording)
 207 this.showRepresentedObject(recording);
 208 }
 209
185210 _handleCanvasAdded(event)
186211 {
187212 this._addCanvas(event.data.canvas);

@@WI.CanvasTabContentView = class CanvasTabContentView extends WI.ContentBrowserTa
213238 if (!recording)
214239 return;
215240
216  this._recordingAdded(recording, {
 241 this._addRecording(recording, {
217242 suppressShowRecording: event.data.fromConsole || this.contentBrowser.currentRepresentedObjects.some((representedObject) => representedObject instanceof WI.Recording),
218243 });
219244 }
220245
221  _recordingAdded(recording, options = {})
222  {
223  if (!recording.source) {
224  const subtitle = null;
225  let recordingTreeElement = new WI.GeneralTreeElement(["recording"], recording.displayName, subtitle, recording);
226  this._importedRecordingsTreeElement.hidden = false;
227  this._importedRecordingsTreeElement.appendChild(recordingTreeElement);
228  }
229 
230  if (!options.suppressShowRecording)
231  this.showRepresentedObject(recording);
232  }
233 
234246 _handleSpace(event)
235247 {
236248 if (WI.isEventTargetAnEditableField(event))

Source/WebInspectorUI/UserInterface/Views/RecordingContentView.js

@@WI.RecordingContentView = class RecordingContentView extends WI.ContentView
246246 snapshot.context.drawImage(snapshot.content, 0, 0);
247247 }
248248
249  for (let name in snapshot.state) {
250  if (!(name in snapshot.context))
251  continue;
252 
253  // Skip internal state used for path debugging.
254  if (name === "currentX" || name === "currentY")
255  continue;
256 
257  try {
258  if (WI.RecordingAction.isFunctionForType(this.representedObject.type, name))
259  snapshot.context[name](...snapshot.state[name]);
260  else
261  snapshot.context[name] = snapshot.state[name];
262  } catch {
263  delete snapshot.state[name];
 249 for (let state of snapshot.states) {
 250 for (let name in state) {
 251 if (!(name in snapshot.context))
 252 continue;
 253
 254 // Skip internal state used for path debugging.
 255 if (name === "currentX" || name === "currentY")
 256 continue;
 257
 258 try {
 259 if (WI.RecordingAction.isFunctionForType(this.representedObject.type, name))
 260 snapshot.context[name](...state[name]);
 261 else
 262 snapshot.context[name] = state[name];
 263 } catch {
 264 delete state[name];
 265 }
264266 }
 267
 268 ++saveCount;
 269 snapshot.context.save();
265270 }
266271
267272 let shouldDrawCanvasPath = showCanvasPath && indexOfLastBeginPathAction <= to;

@@WI.RecordingContentView = class RecordingContentView extends WI.ContentView
291296 else if (actions[i].name === "restore") {
292297 if (!saveCount) // Only attempt to restore if save has been called.
293298 continue;
294  --saveCount;
295299 }
296300
297301 actions[i].apply(snapshot.context);

@@WI.RecordingContentView = class RecordingContentView extends WI.ContentView
353357 let startIndex = 0;
354358 if (lastSnapshotIndex < 0) {
355359 snapshot.content = this._initialContent;
356  snapshot.state = actions[0].state;
 360 snapshot.states = actions[0].states;
357361 } else {
358362 snapshot.content = this._snapshots[lastSnapshotIndex].content;
359  snapshot.state = this._snapshots[lastSnapshotIndex].state;
 363 snapshot.states = this._snapshots[lastSnapshotIndex].states;
360364 startIndex = this._snapshots[lastSnapshotIndex].index;
361365 }
362366
363367 applyActions(startIndex, snapshot.index - 1);
364368 if (snapshot.index > 0)
365  snapshot.state = actions[snapshot.index - 1].state;
 369 snapshot.states = actions[snapshot.index - 1].states;
366370
367371 snapshot.content = new Image;
368372 snapshot.content.src = snapshot.element.toDataURL();

Source/WebInspectorUI/UserInterface/Views/RecordingStateDetailsSidebarPanel.js

@@WI.RecordingStateDetailsSidebarPanel = class RecordingStateDetailsSidebarPanel e
8989
9090 this._dataGrid.removeChildren();
9191
92  console.assert(action.state);
93  if (!action.state)
 92 let currentState = action.states.lastValue;
 93 console.assert(currentState);
 94 if (!currentState)
9495 return;
9596
9697 function isColorProperty(name) {

@@WI.RecordingStateDetailsSidebarPanel = class RecordingStateDetailsSidebarPanel e
106107 return new WI.InlineSwatch(WI.InlineSwatch.Type.Color, color, readOnly);
107108 }
108109
109  for (let name in action.state) {
 110 for (let name in currentState) {
110111 // Skip internal state used for path debugging.
111112 if (name === "setPath")
112113 continue;
113114
114  let value = action.state[name];
 115 let value = currentState[name];
115116 if (typeof value === "object") {
116117 let isGradient = value instanceof CanvasGradient;
117118 let isPattern = value instanceof CanvasPattern;

LayoutTests/ChangeLog

 12018-10-12 Devin Rousso <drousso@apple.com>
 2
 3 Web Inspector: Canvas: capture previously saved states and add them to the recording payload
 4 https://bugs.webkit.org/show_bug.cgi?id=190473
 5
 6 Reviewed by NOBODY (OOPS!).
 7
 8 * inspector/canvas/recording-2d-expected.txt:
 9 * inspector/canvas/recording-2d.html:
 10 * inspector/canvas/resources/recording-utilities.js:
 11 (TestPage.registerInitializer):
 12 (TestPage.registerInitializer.async.logRecording): Added.
 13 (TestPage.registerInitializer.logRecording): Deleted.
 14 * inspector/model/recording-expected.txt:
 15 * inspector/model/recording.html:
 16
1172018-10-08 Yusuke Suzuki <yusukesuzuki@slowstart.org>
218
319 [JSC] JSC should have "parseFunction" to optimize Function constructor

LayoutTests/inspector/canvas/recording-2d-expected.txt

@@initialState:
77 attributes:
88 width: 2
99 height: 2
10  setTransform: [1,0,0,1,0,0]
 10 current state:
 11 setTransform: [[1,0,0,1,0,0]]
1112 globalAlpha: 1
12  globalCompositeOperation: 0
 13 globalCompositeOperation: "source-over"
1314 lineWidth: 1
14  lineCap: 1
15  lineJoin: 2
 15 lineCap: "butt"
 16 lineJoin: "miter"
1617 miterLimit: 10
1718 shadowOffsetX: 0
1819 shadowOffsetY: 0
1920 shadowBlur: 0
20  shadowColor: 3
 21 shadowColor: "rgba(0, 0, 0, 0)"
2122 setLineDash: [[]]
2223 lineDashOffset: 0
23  font: 4
24  textAlign: 5
25  textBaseline: 6
26  direction: 7
27  strokeStyle: 8
28  fillStyle: 8
 24 font: "10px sans-serif"
 25 textAlign: "start"
 26 textBaseline: "alphabetic"
 27 direction: "ltr"
 28 strokeStyle: "#000000"
 29 fillStyle: "#000000"
2930 imageSmoothingEnabled: true
30  imageSmoothingQuality: 9
31  setPath: [10]
 31 imageSmoothingQuality: "low"
 32 setPath: [{}]
3233 parameters:
3334 content: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAAXNSR0IArs4c6QAAAAtJREFUCB1jYEAHAAASAAGAFMrMAAAAAElFTkSuQmCC"
3435frames:

@@initialState:
6566 attributes:
6667 width: 2
6768 height: 2
68  setTransform: [1,0,0,1,0,0]
 69 current state:
 70 setTransform: [[1,0,0,1,0,0]]
6971 globalAlpha: 1
70  globalCompositeOperation: 0
 72 globalCompositeOperation: "source-over"
7173 lineWidth: 1
72  lineCap: 1
73  lineJoin: 2
 74 lineCap: "butt"
 75 lineJoin: "miter"
7476 miterLimit: 10
7577 shadowOffsetX: 0
7678 shadowOffsetY: 0
7779 shadowBlur: 0
78  shadowColor: 3
 80 shadowColor: "rgba(0, 0, 0, 0)"
7981 setLineDash: [[]]
8082 lineDashOffset: 0
81  font: 4
82  textAlign: 5
83  textBaseline: 6
84  direction: 7
85  strokeStyle: 8
86  fillStyle: 8
 83 font: "10px sans-serif"
 84 textAlign: "start"
 85 textBaseline: "alphabetic"
 86 direction: "ltr"
 87 strokeStyle: "#000000"
 88 fillStyle: "#000000"
8789 imageSmoothingEnabled: true
88  imageSmoothingQuality: 9
89  setPath: [10]
 90 imageSmoothingQuality: "low"
 91 setPath: [{}]
9092 parameters:
9193 content: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAAXNSR0IArs4c6QAAAAtJREFUCB1jYEAHAAASAAGAFMrMAAAAAElFTkSuQmCC"
9294frames:

@@initialState:
10711073 attributes:
10721074 width: 2
10731075 height: 2
1074  setTransform: [1,0,0,1,0,0]
 1076 current state:
 1077 setTransform: [[1,0,0,1,0,0]]
10751078 globalAlpha: 1
1076  globalCompositeOperation: 0
 1079 globalCompositeOperation: "source-over"
10771080 lineWidth: 1
1078  lineCap: 1
1079  lineJoin: 2
 1081 lineCap: "butt"
 1082 lineJoin: "miter"
10801083 miterLimit: 10
10811084 shadowOffsetX: 0
10821085 shadowOffsetY: 0
10831086 shadowBlur: 0
1084  shadowColor: 3
 1087 shadowColor: "rgba(0, 0, 0, 0)"
10851088 setLineDash: [[]]
10861089 lineDashOffset: 0
1087  font: 4
1088  textAlign: 5
1089  textBaseline: 6
1090  direction: 7
1091  strokeStyle: 8
1092  fillStyle: 8
 1090 font: "10px sans-serif"
 1091 textAlign: "start"
 1092 textBaseline: "alphabetic"
 1093 direction: "ltr"
 1094 strokeStyle: "#000000"
 1095 fillStyle: "#000000"
10931096 imageSmoothingEnabled: true
1094  imageSmoothingQuality: 9
1095  setPath: [10]
 1097 imageSmoothingQuality: "low"
 1098 setPath: [{}]
10961099 parameters:
10971100 content: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAAXNSR0IArs4c6QAAAAtJREFUCB1jYEAHAAASAAGAFMrMAAAAAElFTkSuQmCC"
10981101frames:

@@PASS: The first frame should have 1 action.
11221125PASS: The action should have 1 parameter.
11231126PASS: The parameter should be null.
11241127
 1128-- Running test case: Canvas.recording2D.ExistingSaves
 1129PASS: There should be four existing states.
 1130PASS: State 0 should match expected fillStyle value.
 1131PASS: State 1 should match expected fillStyle value.
 1132PASS: State 2 should match expected fillStyle value.
 1133PASS: State 3 should match expected fillStyle value.
 1134
11251135-- Running test case: Canvas.recording2D.NoActions
11261136PASS: A recording should have been started and stopped once.
11271137PASS: A recording should have been started and stopped twice.

LayoutTests/inspector/canvas/recording-2d.html

@@async function load() {
4242 document.body.appendChild(canvas);
4343
4444 ctx.save();
45  ctx.save(); // This matches the `restore` call in `performActions`.
 45 cancelActions();
4646
4747 runTest();
4848}

@@function ignoreException(func){
5454}
5555
5656let timeoutID = NaN;
57 let restoreCalled = false;
 57let saveCount = 1;
5858
5959function cancelActions() {
60  if (!isNaN(timeoutID)) {
 60 for (let i = 0; i < saveCount; ++i)
6161 ctx.restore();
62  if (!restoreCalled)
63  ctx.restore();
64  }
 62 ctx.restore(); // Ensures the state is reset between test cases.
6563
6664 clearTimeout(timeoutID);
6765 timeoutID = NaN;
6866
69  ctx.save();
70  ctx.save();
 67 ctx.save(); // Ensures the state is reset between test cases.
 68 ctx.save(); // This matches the `restore` call in `performActions`.
 69 saveCount = 1;
 70
7171 ctx.resetTransform();
7272 ctx.beginPath();
7373 ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
74 
75  restoreCalled = false;
7674}
7775
7876function performActions() {

@@function performActions() {
257255 },
258256 () => {
259257 ctx.restore();
260 
261  restoreCalled = true;
 258 --saveCount;
262259 },
263260 () => {
264261 ctx.rotate(1);
265262 },
266263 () => {
267264 ctx.save();
 265 ++saveCount;
268266 },
269267 () => {
270268 ctx.scale(1, 2);

@@function performConsoleActions() {
408406 ctx.stroke();
409407}
410408
 409function performSavePreActions() {
 410 cancelActions();
 411 ctx.restore();
 412 ctx.restore();
 413
 414 function saveFillStyle(value) {
 415 ctx.save();
 416 ctx.fillStyle = value;
 417 }
 418
 419 saveFillStyle("#ff0000");
 420 saveFillStyle("#00ff00");
 421 saveFillStyle("#0000ff");
 422}
 423
 424function performSavePostActions() {
 425 ctx.fill();
 426}
 427
411428function performNaNActions() {
412429 ctx.globalAlpha = NaN;
413430}

@@function test() {
482499 },
483500 });
484501
 502 suite.addTestCase({
 503 name: "Canvas.recording2D.ExistingSaves",
 504 description: "Check that existing save calls are sent to the frontend.",
 505 test(resolve, reject) {
 506 let canvas = getCanvas(WI.Canvas.ContextType.Canvas2D);
 507 if (!canvas) {
 508 reject("Missing 2D canvas.");
 509 return;
 510 }
 511
 512 async function logStates(recording) {
 513 async function compare(index, expected) {
 514 let swizzledState = await recording._swizzleState(recording.initialState.states[index]);
 515 InspectorTest.expectEqual(swizzledState["fillStyle"], expected, `State ${index} should match expected fillStyle value.`)
 516 }
 517
 518 await compare(0, "#000000");
 519 await compare(1, "#ff0000");
 520 await compare(2, "#00ff00");
 521 await compare(3, "#0000ff");
 522 }
 523
 524 canvas.awaitEvent(WI.Canvas.Event.RecordingStopped)
 525 .then((event) => {
 526 let {recording} = event.data;
 527
 528 InspectorTest.expectEqual(recording.initialState.states.length, 4, "There should be four existing states.");
 529
 530 logStates(recording)
 531 .then(resolve, reject);
 532 });
 533
 534 canvas.awaitEvent(WI.Canvas.Event.RecordingStarted)
 535 .then((event) => {
 536 InspectorTest.evaluateInPage(`performSavePostActions()`).catch(reject);
 537 });
 538
 539 InspectorTest.evaluateInPage(`performSavePreActions()`)
 540 .then(() => {
 541 const singleFrame = true;
 542 CanvasAgent.startRecording(canvas.identifier, singleFrame).catch(reject);
 543 }, reject);
 544 },
 545 });
 546
485547 suite.addTestCase({
486548 name: "Canvas.recording2D.NoActions",
487549 description: "Check that a canvas is still able to be recorded after stopping a recording with no actions.",

LayoutTests/inspector/canvas/resources/recording-utilities.js

@@TestPage.registerInitializer(() => {
44 let value = object[key];
55 if (typeof value === "string")
66 value = sanitizeURL(value);
 7 else if (Array.isArray(value) && value[0] instanceof DOMMatrix)
 8 value[0] = [value[0].a, value[0].b, value[0].c, value[0].d, value[0].e, value[0].f];
79 InspectorTest.log(indent + key + ": " + JSON.stringify(value));
810 }
911 }
1012
11  function logRecording(recording) {
 13 async function logRecording(recording) {
1214 InspectorTest.log("initialState:");
1315
1416 InspectorTest.log(" attributes:");
1517 log(recording.initialState.attributes, " ");
1618
 19 let currentState = recording.initialState.states.lastValue;
 20 if (currentState) {
 21 InspectorTest.log(" current state:");
 22 let swizzledState = await recording._swizzleState(currentState);
 23 log(swizzledState, " ");
 24 }
 25
1726 InspectorTest.log(" parameters:");
1827 log(recording.initialState.parameters, " ");
1928

@@TestPage.registerInitializer(() => {
92101 if (!stopped)
93102 CanvasAgent.stopRecording(canvas.identifier).catch(reject);
94103 else {
95  InspectorTest.evaluateInPage(`cancelActions()`).catch(reject);
96 
97  if (swizzled)
98  resolve();
 104 InspectorTest.evaluateInPage(`cancelActions()`)
 105 .then(() => {
 106 if (swizzled)
 107 resolve();
 108 }, reject);
99109 }
100110 });
101111

@@TestPage.registerInitializer(() => {
124134 Promise.all(recording.actions.map((action) => action.swizzle(recording))).then(() => {
125135 swizzled = true;
126136
127  logRecording(recording, type);
128 
129  if (lastFrame) {
130  InspectorTest.evaluateInPage(`cancelActions()`)
131  .then(resolve, reject);
132  }
 137 logRecording(recording, type)
 138 .then(() => {
 139 if (lastFrame) {
 140 InspectorTest.evaluateInPage(`cancelActions()`)
 141 .then(resolve, reject);
 142 }
 143 }, reject);
133144 });
134145 });
135146

LayoutTests/inspector/model/recording-expected.txt

@@null
3737 "attributes": {
3838 "test": "test"
3939 },
 40 "states": [
 41 {
 42 "test": "test"
 43 }
 44 ],
4045 "parameters": [
4146 "test"
4247 ],

@@null
6065 "attributes": {
6166 "test": "test"
6267 },
 68 "states": [
 69 {
 70 "test": "test"
 71 }
 72 ],
6373 "parameters": [
6474 "test"
6575 ],

@@null
92102 "attributes": {
93103 "test": "test"
94104 },
 105 "states": [
 106 {
 107 "test": "test"
 108 }
 109 ],
95110 "parameters": [
96111 "test"
97112 ],

@@null
124139 "attributes": {
125140 "test": "test"
126141 },
 142 "states": [
 143 {
 144 "test": "test"
 145 }
 146 ],
127147 "parameters": [
128148 "test"
129149 ],

LayoutTests/inspector/model/recording.html

@@function test()
4848 type: "test",
4949 initialState: {
5050 attributes: null,
 51 states: null,
5152 parameters: null,
5253 content: null,
5354 },

@@function test()
6465 attributes: {
6566 test: "test",
6667 },
 68 states: [
 69 {
 70 test: "test",
 71 },
 72 ],
6773 parameters: ["test"],
6874 content: "test",
6975 },

@@function test()
8692 attributes: {
8793 test: "test",
8894 },
 95 states: [
 96 {
 97 test: "test",
 98 },
 99 ],
89100 parameters: ["test"],
90101 content: "test",
91102 },

@@function test()
108119 attributes: {
109120 test: "test",
110121 },
 122 states: [
 123 {
 124 test: "test",
 125 },
 126 ],
111127 parameters: ["test"],
112128 content: "test",
113129 },

@@function test()
137153 attributes: {
138154 test: "test",
139155 },
 156 states: [
 157 {
 158 test: "test",
 159 },
 160 ],
140161 parameters: ["test"],
141162 content: "test",
142163 },