Bug 1544713 - make full page audit non-blocking. Fire 'audit-event' events when the full page audit completes or fails. r=gl
authorYura Zenevich <yura.zenevich@gmail.com>
Mon, 29 Apr 2019 21:08:31 +0000
changeset 530654 143b77783b2e662853ea6dc151043ebb803bde6d
parent 530653 814e0d96684235d7e6a2024b9da8e8a0acbe6d59
child 530655 db10a6b3d728c9113c13c1b92d01a56a3f947c1d
push id11265
push userffxbld-merge
push dateMon, 13 May 2019 10:53:39 +0000
treeherdermozilla-beta@77e0fe8dbdd3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgl
bugs1544713
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1544713 - make full page audit non-blocking. Fire 'audit-event' events when the full page audit completes or fails. r=gl Differential Revision: https://phabricator.services.mozilla.com/D28854
devtools/client/accessibility/actions/audit.js
devtools/server/actors/accessibility/walker.js
devtools/server/tests/browser/browser_accessibility_walker_audit.js
devtools/server/tests/browser/doc_accessibility_audit.html
devtools/server/tests/browser/head.js
devtools/shared/specs/accessibility.js
--- a/devtools/client/accessibility/actions/audit.js
+++ b/devtools/client/accessibility/actions/audit.js
@@ -8,11 +8,16 @@ const { AUDIT, AUDITING, FILTER_TOGGLE }
 
 exports.filterToggle = filter =>
   dispatch => dispatch({ filter, type: FILTER_TOGGLE });
 
 exports.auditing = filter =>
   dispatch => dispatch({ auditing: filter, type: AUDITING });
 
 exports.audit = (walker, filter) =>
-  dispatch => walker.audit()
-    .then(response => dispatch({ type: AUDIT, response }))
-    .catch(error => dispatch({ type: AUDIT, error }));
+  dispatch => {
+    const onAuditEvent = walker.once("audit-event");
+    walker.startAudit();
+    return onAuditEvent
+      .then(({ ancestries: response, error }) =>
+        dispatch({ type: AUDIT, error, response }))
+      .catch(error => dispatch({ type: AUDIT, error }));
+  };
--- a/devtools/server/actors/accessibility/walker.js
+++ b/devtools/server/actors/accessibility/walker.js
@@ -412,16 +412,37 @@ const AccessibleWalkerActor = ActorClass
             check.score === accessibility.SCORES.FAIL)) {
         ancestries.push(this.getAncestry(acc));
       }
     }
 
     return Promise.all(ancestries);
   },
 
+  /**
+   * Start accessibility audit. The result of this function will not be an audit
+   * report. Instead, an "audit-event" event will be fired when the audit is
+   * completed or fails.
+   */
+  startAudit() {
+    // Audit is already running, wait for the "audit-event" event.
+    if (this._auditing) {
+      return;
+    }
+
+    this._auditing = this.audit()
+      // We do not want to block on audit request, instead fire "audit-event"
+      // event when internal audit is finished or failed.
+      .then(ancestries => this.emit("audit-event", { ancestries }))
+      .catch(() => this.emit("audit-event", { error: true }))
+      .finally(() => {
+        this._auditing = null;
+      });
+  },
+
   onHighlighterEvent: function(data) {
     this.emit("highlighter-event", data);
   },
 
   /**
    * Accessible event observer function.
    *
    * @param {Ci.nsIAccessibleEvent} subject
--- a/devtools/server/tests/browser/browser_accessibility_walker_audit.js
+++ b/devtools/server/tests/browser/browser_accessibility_walker_audit.js
@@ -14,67 +14,72 @@ add_task(async function() {
     role: "document",
     childCount: 2,
     checks: {
       "CONTRAST": null,
     },
   }, {
     name: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do " +
           "eiusmod tempor incididunt ut labore et dolore magna aliqua.",
-    role: "heading",
+    role: "paragraph",
     childCount: 1,
     checks: {
       "CONTRAST": null,
     },
   }, {
     name: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do " +
            "eiusmod tempor incididunt ut labore et dolore magna aliqua.",
     role: "text leaf",
     childCount: 0,
     checks: {
       "CONTRAST": {
-        "value": 21,
-        "color": [0, 0, 0, 1],
+        "value": 4.00,
+        "color": [255, 0, 0, 1],
         "backgroundColor": [255, 255, 255, 1],
-        "isLargeText": true,
+        "isLargeText": false,
+        "score": "fail",
       },
     },
   }, {
     name: "",
     role: "paragraph",
     childCount: 1,
     checks: {
       "CONTRAST": null,
     },
   }, {
     name: "Accessible Paragraph",
     role: "text leaf",
     childCount: 0,
     checks: {
       "CONTRAST": {
-        "value": 21,
-        "color": [0, 0, 0, 1],
+        "value": 4.00,
+        "color": [255, 0, 0, 1],
         "backgroundColor": [255, 255, 255, 1],
         "isLargeText": false,
+        "score": "fail",
       },
     },
   }];
 
   function findAccessible(name, role) {
     return accessibles.find(accessible =>
       accessible.name === name && accessible.role === role);
   }
 
   const a11yWalker = await accessibility.getWalker();
   ok(a11yWalker, "The AccessibleWalkerFront was returned");
   await accessibility.enable();
 
   info("Checking AccessibleWalker audit functionality");
-  const ancestries = await a11yWalker.audit();
+  const auditEvent = a11yWalker.once("audit-event");
+  a11yWalker.startAudit();
+  const { ancestries } = await auditEvent;
 
+  is(ancestries.length, 2, "The size of ancestries is correct");
   for (const ancestry of ancestries) {
     for (const { accessible, children } of ancestry) {
       checkA11yFront(accessible,
                      findAccessible(accessibles.name, accessibles.role));
       for (const child of children) {
         checkA11yFront(child,
                        findAccessible(child.name, child.role));
       }
--- a/devtools/server/tests/browser/doc_accessibility_audit.html
+++ b/devtools/server/tests/browser/doc_accessibility_audit.html
@@ -1,10 +1,10 @@
 <!DOCTYPE HTML>
 <html>
   <head>
     <meta charset="utf-8">
   </head>
-<body>
-  <h1 id="h1">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</h1>
-  <p id="p">Accessible Paragraph</p>
+<body style="color: red;">
+  <p id="p1">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
+  <p id="p2">Accessible Paragraph</p>
 </body>
 </html>
--- a/devtools/server/tests/browser/head.js
+++ b/devtools/server/tests/browser/head.js
@@ -266,17 +266,27 @@ async function emitA11yEvent(emitter, na
  */
 function checkA11yFront(front, expected, expectedFront) {
   ok(front, "The accessibility front is created");
 
   if (expectedFront) {
     is(front, expectedFront, "Matching accessibility front");
   }
 
+  // Clone the front so we could modify some values for comparison.
+  front = Object.assign(front);
   for (const key in expected) {
+    if (key === "checks") {
+      const { CONTRAST } = front[key];
+      // Contrast values are rounded to two digits after the decimal point.
+      if (CONTRAST && CONTRAST.value) {
+        CONTRAST.value = parseFloat(CONTRAST.value.toFixed(2));
+      }
+    }
+
     if (["actions", "states", "attributes", "checks"].includes(key)) {
       SimpleTest.isDeeply(front[key], expected[key],
         `Accessible Front has correct ${key}`);
     } else {
       is(front[key], expected[key], `accessibility front has correct ${key}`);
     }
   }
 }
--- a/devtools/shared/specs/accessibility.js
+++ b/devtools/shared/specs/accessibility.js
@@ -16,16 +16,28 @@ types.addActorType("accessible");
 types.addDictType("accessibleWithChildren", {
   // Accessible
   accessible: "accessible",
   // Accessible's children
   children: "array:accessible",
 });
 
 /**
+ * Data passed via "audit-event" to the client. It may include a list of
+ * ancestries for accessible actors that have failing accessibility checks or
+ * an error flag.
+ */
+types.addDictType("auditEventData", {
+  // List of ancestries (array:accessibleWithChildren)
+  ancestries: "array:array:accessibleWithChildren",
+  // True if the audit failed.
+  error: "boolean",
+});
+
+/**
  * Accessible relation object described by its type that also includes relation targets.
  */
 types.addDictType("accessibleRelation", {
   // Accessible relation type
   type: "string",
   // Accessible relation's targets
   targets: "array:accessible",
 });
@@ -142,16 +154,20 @@ const accessibleWalkerSpec = generateAct
     },
     "picker-accessible-canceled": {
       type: "pickerAccessibleCanceled",
     },
     "highlighter-event": {
       type: "highlighter-event",
       data: Arg(0, "json"),
     },
+    "audit-event": {
+      type: "audit-event",
+      audit: Arg(0, "auditEventData"),
+    },
   },
 
   methods: {
     children: {
       request: {},
       response: {
         children: RetVal("array:accessible"),
       },
@@ -163,22 +179,17 @@ const accessibleWalkerSpec = generateAct
       },
     },
     getAncestry: {
       request: { accessible: Arg(0, "accessible") },
       response: {
         ancestry: RetVal("array:accessibleWithChildren"),
       },
     },
-    audit: {
-      request: {},
-      response: {
-        audit: RetVal("array:array:accessibleWithChildren"),
-      },
-    },
+    startAudit: {},
     highlightAccessible: {
       request: {
         accessible: Arg(0, "accessible"),
         options: Arg(1, "nullable:json"),
       },
       response: {
         value: RetVal("nullable:boolean"),
       },