Web Inspector: Audit: allow audits to be enabled/disabled
authordrousso@apple.com <drousso@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 11 Jan 2019 05:54:54 +0000 (05:54 +0000)
committerdrousso@apple.com <drousso@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 11 Jan 2019 05:54:54 +0000 (05:54 +0000)
https://bugs.webkit.org/show_bug.cgi?id=192210
<rdar://problem/46423583>

Reviewed by Joseph Pecoraro.

Source/WebInspectorUI:

* UserInterface/Controllers/AuditManager.js:
(WI.AuditManager.prototype.get editing): Added.
(WI.AuditManager.prototype.set editing): Added.
(WI.AuditManager.prototype.stop):
(WI.AuditManager.prototype.addDefaultTestsIfNeeded):
Since default audits aren't stored, keep a list of disabled default tests in a `WI.Setting`.

* UserInterface/Models/AuditTestBase.js:
(WI.AuditTestBase):
(WI.AuditTestBase.prototype.get disabled): Added.
(WI.AuditTestBase.prototype.set disabled): Added.
(WI.AuditTestBase.prototype.async start):
(WI.AuditTestBase.prototype.stop):
(WI.AuditTestBase.toJSON):

* UserInterface/Models/AuditTestCase.js:
(WI.AuditTestCase):
(WI.AuditTestCase.async fromPayload):
(WI.AuditTestCase.prototype.toJSON):

* UserInterface/Models/AuditTestGroup.js:
(WI.AuditTestGroup):
(WI.AuditTestGroup.async fromPayload):
(WI.AuditTestGroup.prototype.get disabled): Added.
(WI.AuditTestGroup.prototype.set disabled): Added.
(WI.AuditTestGroup.prototype.toJSON):
(WI.AuditTestGroup.prototype.async run):
(WI.AuditTestGroup.prototype._handleTestDisabledChanged): Added.
(WI.AuditTestGroup.prototype._handleTestProgress):
Propagate `disabled` changes to all sub-tests, unless the change was caused by one of the
sub-tests, in which case we are now in an intermediate state.

* UserInterface/Views/AuditNavigationSidebarPanel.js:
(WI.AuditNavigationSidebarPanel):
(WI.AuditNavigationSidebarPanel.prototype.showDefaultContentView):
(WI.AuditNavigationSidebarPanel.prototype.initialLayout):
(WI.AuditNavigationSidebarPanel.prototype.hasCustomFilters): Added.
(WI.AuditNavigationSidebarPanel.prototype.matchTreeElementAgainstCustomFilters): Added.
(WI.AuditNavigationSidebarPanel.prototype._addTest):
(WI.AuditNavigationSidebarPanel.prototype._addResult):
(WI.AuditNavigationSidebarPanel.prototype._updateStartStopButtonNavigationItemState):
(WI.AuditNavigationSidebarPanel.prototype._updateEditButtonNavigationItemState): Added.
(WI.AuditNavigationSidebarPanel.prototype._handleAuditManagerEditingChanged): Added.
(WI.AuditNavigationSidebarPanel.prototype._handleAuditTestRemoved):
(WI.AuditNavigationSidebarPanel.prototype._handleAuditTestScheduled):
(WI.AuditNavigationSidebarPanel.prototype._treeSelectionDidChange):
(WI.AuditNavigationSidebarPanel.prototype._handleEditButtonNavigationItemClicked): Added.
* UserInterface/Views/AuditNavigationSidebarPanel.css:
(.sidebar > .panel.navigation.audit > .content):
(.sidebar > .panel.navigation.audit > .content > .tree-outline): Added.
(.sidebar > .panel.navigation.audit > .content .edit-audits:not(.disabled):active): Added.
(.sidebar > .panel.navigation.audit > .content .edit-audits:not(.disabled).activated): Added.
(.sidebar > .panel.navigation.audit > .content .edit-audits:not(.disabled).activated:active): Added.
(.sidebar > .panel.navigation.audit > .content .edit-audits.disabled): Added.
(.finish-editing-audits-placeholder.message-text-view .navigation-item-help .navigation-bar): Added.
Leverage custom filters to ensure that disabled audits arent shown when not editing and that
result tree elements aren't shown while editing.

* UserInterface/Views/AuditTestGroupContentView.js:
(WI.AuditTestGroupContentView.prototype.shown):

* UserInterface/Views/AuditTreeElement.js:
(WI.AuditTreeElement.prototype.onattach):
(WI.AuditTreeElement.prototype.canSelectOnMouseDown): Added.
(WI.AuditTreeElement.prototype._updateTestGroupDisabled): Added.
(WI.AuditTreeElement.prototype._handleTestDisabledChanged): Added.
(WI.AuditTreeElement.prototype._handleManagerEditingChanged): Added.
* UserInterface/Views/AuditTreeElement.css:
(.tree-outline .item.audit > .status:not(:hover) > img.show-on-hover, .tree-outline .item.audit.test-group.expanded:not(.editing-audits) > .status:not(:hover)): Added.
(.tree-outline .item.audit.manager-active > .status > img.show-on-hover, .tree-outline .item.audit.test-group.expanded:not(.editing-audits) > .status:hover > :not(img), .tree-outline .item.audit.test-group-result.expanded > .status): Added.
(.tree-outline .item.audit > .status:not(:hover) > img.show-on-hover, .tree-outline .item.audit.test-group.expanded > .status:not(:hover)): Deleted.
(.tree-outline .item.audit.manager-active > .status > img.show-on-hover, .tree-outline .item.audit.test-group.expanded > .status:hover > :not(img), .tree-outline .item.audit.test-group-result.expanded > .status): Deleted.
Prevent selection and running when editing.

* UserInterface/Views/TreeOutline.css:
(.tree-outline .children.expanded:not([hidden])): Added.
(.tree-outline .children.expanded): Deleted.

* UserInterface/Base/ObjectStore.js:
(WI.ObjectStore._open):
Batch operations together to help avoid multiple simultaneous `indexedDB.open` calls. This
should also help preserve the order of operations, as once the database is open, operations
are executed in the order they were enqueued.

(WI.ObjectStore.prototype.async.addObject):
Pass a unique `Symbol` to the `toJSON` call on the given object so that the object can save
additional values that wouldn't normally be saved. This doesn't conflict with normal usage
of `toJSON` (e.g. `JSON.stringify`) because that case also passes in a value:
 - `undefined`, if it was called directly on the object
 - the key for this object in the containing object
 - the index of this object in the containing array
In any case, the value can never equal the unique `Symbol`, so it's guaranteed that the code
will only run for `WI.ObjectStore` operations.

(WI.ObjectStore.prototype.async.clear): Added.

* Localizations/en.lproj/localizedStrings.js:

LayoutTests:

* inspector/unit-tests/objectStore/clear.html: Added.
* inspector/unit-tests/objectStore/clear-expected.txt: Added.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@239858 268f45cc-cd09-0410-ab3c-d52691b4dbfc

16 files changed:
LayoutTests/ChangeLog
LayoutTests/inspector/unit-tests/objectStore/clear-expected.txt [new file with mode: 0644]
LayoutTests/inspector/unit-tests/objectStore/clear.html [new file with mode: 0644]
Source/WebInspectorUI/ChangeLog
Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js
Source/WebInspectorUI/UserInterface/Base/ObjectStore.js
Source/WebInspectorUI/UserInterface/Controllers/AuditManager.js
Source/WebInspectorUI/UserInterface/Models/AuditTestBase.js
Source/WebInspectorUI/UserInterface/Models/AuditTestCase.js
Source/WebInspectorUI/UserInterface/Models/AuditTestGroup.js
Source/WebInspectorUI/UserInterface/Views/AuditNavigationSidebarPanel.css
Source/WebInspectorUI/UserInterface/Views/AuditNavigationSidebarPanel.js
Source/WebInspectorUI/UserInterface/Views/AuditTestGroupContentView.js
Source/WebInspectorUI/UserInterface/Views/AuditTreeElement.css
Source/WebInspectorUI/UserInterface/Views/AuditTreeElement.js
Source/WebInspectorUI/UserInterface/Views/TreeOutline.css

index 9dc92ef..583c14a 100644 (file)
@@ -1,3 +1,14 @@
+2019-01-10  Devin Rousso  <drousso@apple.com>
+
+        Web Inspector: Audit: allow audits to be enabled/disabled
+        https://bugs.webkit.org/show_bug.cgi?id=192210
+        <rdar://problem/46423583>
+
+        Reviewed by Joseph Pecoraro.
+
+        * inspector/unit-tests/objectStore/clear.html: Added.
+        * inspector/unit-tests/objectStore/clear-expected.txt: Added.
+
 2019-01-10  Justin Fan  <justin_fan@apple.com>
 
         [WebGPU] WebGPUBindGroup and device::createBindGroup prototype
diff --git a/LayoutTests/inspector/unit-tests/objectStore/clear-expected.txt b/LayoutTests/inspector/unit-tests/objectStore/clear-expected.txt
new file mode 100644 (file)
index 0000000..babfb79
--- /dev/null
@@ -0,0 +1,12 @@
+Tests WI.ObjectStore.prototype.clear.
+
+
+== Running test suite: WI.ObjectStore.prototype.clear
+-- Running test case: WI.ObjectStore.prototype.clear.Empty
+[]
+[]
+
+-- Running test case: WI.ObjectStore.prototype.clear.NotEmpty
+[true,1,"foo",["bar"],{"a":1}]
+[]
+
diff --git a/LayoutTests/inspector/unit-tests/objectStore/clear.html b/LayoutTests/inspector/unit-tests/objectStore/clear.html
new file mode 100644 (file)
index 0000000..d9e714e
--- /dev/null
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="../../../http/tests/inspector/resources/inspector-test.js"></script>
+<script src="resources/objectStore-utilities.js"></script>
+<script>
+function test()
+{
+    let suite = InspectorTest.ObjectStore.createSuite("WI.ObjectStore.prototype.clear");
+
+    function testClear(name, {objects}) {
+        InspectorTest.ObjectStore.wrapTest(name, async function() {
+            let objectStore = InspectorTest.ObjectStore.createObjectStore({autoIncrement: true});
+
+            for (let object of objects)
+                await objectStore.add(object);
+
+            await InspectorTest.ObjectStore.logValues();
+
+            await objectStore.clear();
+        });
+    }
+
+    testClear("WI.ObjectStore.prototype.clear.Empty", {
+        objects: [],
+    });
+
+    testClear("WI.ObjectStore.prototype.clear.NotEmpty", {
+        objects: [true, 1, "foo", ["bar"], {a: 1}],
+    });
+
+    suite.runTestCasesAndFinish();
+}
+</script>
+</head>
+<body onload="runTest()">
+    <p>Tests WI.ObjectStore.prototype.clear.</p>
+</body>
+</html>
index 1015bff..819fc0b 100644 (file)
@@ -1,3 +1,109 @@
+2019-01-10  Devin Rousso  <drousso@apple.com>
+
+        Web Inspector: Audit: allow audits to be enabled/disabled
+        https://bugs.webkit.org/show_bug.cgi?id=192210
+        <rdar://problem/46423583>
+
+        Reviewed by Joseph Pecoraro.
+
+        * UserInterface/Controllers/AuditManager.js:
+        (WI.AuditManager.prototype.get editing): Added.
+        (WI.AuditManager.prototype.set editing): Added.
+        (WI.AuditManager.prototype.stop):
+        (WI.AuditManager.prototype.addDefaultTestsIfNeeded):
+        Since default audits aren't stored, keep a list of disabled default tests in a `WI.Setting`.
+
+        * UserInterface/Models/AuditTestBase.js:
+        (WI.AuditTestBase):
+        (WI.AuditTestBase.prototype.get disabled): Added.
+        (WI.AuditTestBase.prototype.set disabled): Added.
+        (WI.AuditTestBase.prototype.async start):
+        (WI.AuditTestBase.prototype.stop):
+        (WI.AuditTestBase.toJSON):
+
+        * UserInterface/Models/AuditTestCase.js:
+        (WI.AuditTestCase):
+        (WI.AuditTestCase.async fromPayload):
+        (WI.AuditTestCase.prototype.toJSON):
+
+        * UserInterface/Models/AuditTestGroup.js:
+        (WI.AuditTestGroup):
+        (WI.AuditTestGroup.async fromPayload):
+        (WI.AuditTestGroup.prototype.get disabled): Added.
+        (WI.AuditTestGroup.prototype.set disabled): Added.
+        (WI.AuditTestGroup.prototype.toJSON):
+        (WI.AuditTestGroup.prototype.async run):
+        (WI.AuditTestGroup.prototype._handleTestDisabledChanged): Added.
+        (WI.AuditTestGroup.prototype._handleTestProgress):
+        Propagate `disabled` changes to all sub-tests, unless the change was caused by one of the
+        sub-tests, in which case we are now in an intermediate state.
+
+        * UserInterface/Views/AuditNavigationSidebarPanel.js:
+        (WI.AuditNavigationSidebarPanel):
+        (WI.AuditNavigationSidebarPanel.prototype.showDefaultContentView):
+        (WI.AuditNavigationSidebarPanel.prototype.initialLayout):
+        (WI.AuditNavigationSidebarPanel.prototype.hasCustomFilters): Added.
+        (WI.AuditNavigationSidebarPanel.prototype.matchTreeElementAgainstCustomFilters): Added.
+        (WI.AuditNavigationSidebarPanel.prototype._addTest):
+        (WI.AuditNavigationSidebarPanel.prototype._addResult):
+        (WI.AuditNavigationSidebarPanel.prototype._updateStartStopButtonNavigationItemState):
+        (WI.AuditNavigationSidebarPanel.prototype._updateEditButtonNavigationItemState): Added.
+        (WI.AuditNavigationSidebarPanel.prototype._handleAuditManagerEditingChanged): Added.
+        (WI.AuditNavigationSidebarPanel.prototype._handleAuditTestRemoved):
+        (WI.AuditNavigationSidebarPanel.prototype._handleAuditTestScheduled):
+        (WI.AuditNavigationSidebarPanel.prototype._treeSelectionDidChange):
+        (WI.AuditNavigationSidebarPanel.prototype._handleEditButtonNavigationItemClicked): Added.
+        * UserInterface/Views/AuditNavigationSidebarPanel.css:
+        (.sidebar > .panel.navigation.audit > .content):
+        (.sidebar > .panel.navigation.audit > .content > .tree-outline): Added.
+        (.sidebar > .panel.navigation.audit > .content .edit-audits:not(.disabled):active): Added.
+        (.sidebar > .panel.navigation.audit > .content .edit-audits:not(.disabled).activated): Added.
+        (.sidebar > .panel.navigation.audit > .content .edit-audits:not(.disabled).activated:active): Added.
+        (.sidebar > .panel.navigation.audit > .content .edit-audits.disabled): Added.
+        (.finish-editing-audits-placeholder.message-text-view .navigation-item-help .navigation-bar): Added.
+        Leverage custom filters to ensure that disabled audits arent shown when not editing and that
+        result tree elements aren't shown while editing.
+
+        * UserInterface/Views/AuditTestGroupContentView.js:
+        (WI.AuditTestGroupContentView.prototype.shown):
+
+        * UserInterface/Views/AuditTreeElement.js:
+        (WI.AuditTreeElement.prototype.onattach):
+        (WI.AuditTreeElement.prototype.canSelectOnMouseDown): Added.
+        (WI.AuditTreeElement.prototype._updateTestGroupDisabled): Added.
+        (WI.AuditTreeElement.prototype._handleTestDisabledChanged): Added.
+        (WI.AuditTreeElement.prototype._handleManagerEditingChanged): Added.
+        * UserInterface/Views/AuditTreeElement.css:
+        (.tree-outline .item.audit > .status:not(:hover) > img.show-on-hover, .tree-outline .item.audit.test-group.expanded:not(.editing-audits) > .status:not(:hover)): Added.
+        (.tree-outline .item.audit.manager-active > .status > img.show-on-hover, .tree-outline .item.audit.test-group.expanded:not(.editing-audits) > .status:hover > :not(img), .tree-outline .item.audit.test-group-result.expanded > .status): Added.
+        (.tree-outline .item.audit > .status:not(:hover) > img.show-on-hover, .tree-outline .item.audit.test-group.expanded > .status:not(:hover)): Deleted.
+        (.tree-outline .item.audit.manager-active > .status > img.show-on-hover, .tree-outline .item.audit.test-group.expanded > .status:hover > :not(img), .tree-outline .item.audit.test-group-result.expanded > .status): Deleted.
+        Prevent selection and running when editing.
+
+        * UserInterface/Views/TreeOutline.css:
+        (.tree-outline .children.expanded:not([hidden])): Added.
+        (.tree-outline .children.expanded): Deleted.
+
+        * UserInterface/Base/ObjectStore.js:
+        (WI.ObjectStore._open):
+        Batch operations together to help avoid multiple simultaneous `indexedDB.open` calls. This
+        should also help preserve the order of operations, as once the database is open, operations
+        are executed in the order they were enqueued.
+
+        (WI.ObjectStore.prototype.async.addObject):
+        Pass a unique `Symbol` to the `toJSON` call on the given object so that the object can save
+        additional values that wouldn't normally be saved. This doesn't conflict with normal usage
+        of `toJSON` (e.g. `JSON.stringify`) because that case also passes in a value:
+         - `undefined`, if it was called directly on the object
+         - the key for this object in the containing object
+         - the index of this object in the containing array
+        In any case, the value can never equal the unique `Symbol`, so it's guaranteed that the code
+        will only run for `WI.ObjectStore` operations.
+
+        (WI.ObjectStore.prototype.async.clear): Added.
+
+        * Localizations/en.lproj/localizedStrings.js:
+
 2019-01-09  Devin Rousso  <drousso@apple.com>
 
         Web Inspector: Protocol Logging: log messages as objects if inspector^2 is open
index 90fa41e..61d2f6d 100644 (file)
@@ -324,6 +324,7 @@ localizedStrings["Edit \u201Ccubic-bezier\u201D function"] = "Edit \u201Ccubic-b
 localizedStrings["Edit \u201Cspring\u201D function"] = "Edit \u201Cspring\u201D function";
 localizedStrings["Edit configuration"] = "Edit configuration";
 localizedStrings["Edit custom gradient"] = "Edit custom gradient";
+localizedStrings["Editing audits"] = "Editing audits";
 localizedStrings["Element"] = "Element";
 localizedStrings["Element clips compositing descendants"] = "Element clips compositing descendants";
 localizedStrings["Element has CSS blending applied and composited descendants"] = "Element has CSS blending applied and composited descendants";
@@ -688,6 +689,7 @@ localizedStrings["Preserve Log"] = "Preserve Log";
 localizedStrings["Press %s to import a test or result file"] = "Press %s to import a test or result file";
 localizedStrings["Press %s to load a recording from file."] = "Press %s to load a recording from file.";
 localizedStrings["Press %s to start running the audit"] = "Press %s to start running the audit";
+localizedStrings["Press %s to stop editing"] = "Press %s to stop editing";
 localizedStrings["Pressed"] = "Pressed";
 localizedStrings["Pretty print"] = "Pretty print";
 localizedStrings["Preview"] = "Preview";
index b7bf893..c03ed8d 100644 (file)
@@ -52,6 +52,13 @@ WI.ObjectStore = class ObjectStore
             return;
         }
 
+        if (Array.isArray(WI.ObjectStore._databaseCallbacks)) {
+            WI.ObjectStore._databaseCallbacks.push(callback);
+            return;
+        }
+
+        WI.ObjectStore._databaseCallbacks = [callback];
+
         const version = 1; // Increment this for every edit to `WI.objectStores`.
 
         let databaseRequest = indexedDB.open(WI.ObjectStore._databaseName, version);
@@ -81,7 +88,10 @@ WI.ObjectStore = class ObjectStore
                 WI.ObjectStore._database = null;
             });
 
-            callback(WI.ObjectStore._database);
+            for (let databaseCallback of WI.ObjectStore._databaseCallbacks)
+                databaseCallback(WI.ObjectStore._database);
+
+            WI.ObjectStore._databaseCallbacks = null;
         });
     }
 
@@ -118,7 +128,7 @@ WI.ObjectStore = class ObjectStore
             return undefined;
 
         console.assert(typeof object.toJSON === "function", "ObjectStore cannot store an object without JSON serialization", object.constructor.name);
-        let result = await this.add(object.toJSON(), ...args);
+        let result = await this.add(object.toJSON(WI.ObjectStore.toJSONSymbol), ...args);
         this.associateObject(object, args[0], result);
         return result;
     }
@@ -139,6 +149,14 @@ WI.ObjectStore = class ObjectStore
         return this.delete(this._resolveKeyPath(object).value, ...args);
     }
 
+    async clear(...args)
+    {
+        if (!WI.ObjectStore.supported())
+            return undefined;
+
+        return this._operation("readwrite", (objectStore) => objectStore.clear(...args));
+    }
+
     // Private
 
     _resolveKeyPath(object, keyPath)
@@ -203,6 +221,9 @@ WI.ObjectStore = class ObjectStore
 };
 
 WI.ObjectStore._database = null;
+WI.ObjectStore._databaseCallbacks = null;
+
+WI.ObjectStore.toJSONSymbol = Symbol("ObjectStore-toJSON");
 
 // Be sure to update the `version` above when making changes.
 WI.objectStores = {
index 7684b4d..6d0c7e6 100644 (file)
@@ -35,6 +35,8 @@ WI.AuditManager = class AuditManager extends WI.Object
         this._runningState = WI.AuditManager.RunningState.Inactive;
         this._runningTests = [];
 
+        this._disabledDefaultTestsSetting = new WI.Setting("audit-disabled-default-tests", []);
+
         WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._handleFrameMainResourceDidChange, this);
     }
 
@@ -52,6 +54,51 @@ WI.AuditManager = class AuditManager extends WI.Object
     get results() { return this._results; }
     get runningState() { return this._runningState; }
 
+    get editing()
+    {
+        return this._runningState === WI.AuditManager.RunningState.Disabled;
+    }
+
+    set editing(editing)
+    {
+        console.assert(this._runningState === WI.AuditManager.RunningState.Disabled || this._runningState === WI.AuditManager.RunningState.Inactive);
+        if (this._runningState !== WI.AuditManager.RunningState.Disabled && this._runningState !== WI.AuditManager.RunningState.Inactive)
+            return;
+
+        let runningState = editing ? WI.AuditManager.RunningState.Disabled : WI.AuditManager.RunningState.Inactive;
+        console.assert(runningState !== this._runningState);
+        if (runningState === this._runningState)
+            return;
+
+        this._runningState = runningState;
+
+        this.dispatchEventToListeners(WI.AuditManager.Event.EditingChanged);
+
+        if (!this.editing) {
+            WI.objectStores.audits.clear();
+
+            let disabledDefaultTests = [];
+            let saveDisabledDefaultTest = (test) => {
+                if (test.disabled)
+                    disabledDefaultTests.push(test.name);
+
+                if (test instanceof WI.AuditTestGroup) {
+                    for (let child of test.tests)
+                        saveDisabledDefaultTest(child);
+                }
+            };
+
+            for (let test of this._tests) {
+                if (test.__default)
+                    saveDisabledDefaultTest(test);
+                else
+                    WI.objectStores.audits.addObject(test);
+            }
+
+            this._disabledDefaultTestsSetting.value = disabledDefaultTests;
+        }
+    }
+
     async start(tests)
     {
         console.assert(this._runningState === WI.AuditManager.RunningState.Inactive);
@@ -97,10 +144,10 @@ WI.AuditManager = class AuditManager extends WI.Object
         if (this._runningState !== WI.AuditManager.RunningState.Active)
             return;
 
+        this._runningState = WI.AuditManager.RunningState.Stopping;
+
         for (let test of this._runningTests)
             test.stop();
-
-        this._runningState = WI.AuditManager.RunningState.Stopping;
     }
 
     async processJSON({json, error})
@@ -253,7 +300,19 @@ WI.AuditManager = class AuditManager extends WI.Object
             ], {description: WI.UIString("Tests for ways to improve accessibility.")}),
         ];
 
+        let checkDisabledDefaultTest = (test) => {
+            if (this._disabledDefaultTestsSetting.value.includes(test.name))
+                test.disabled = true;
+
+            if (test instanceof WI.AuditTestGroup) {
+                for (let child of test.tests)
+                    checkDisabledDefaultTest(child);
+            }
+        };
+
         for (let test of defaultTests) {
+            checkDisabledDefaultTest(test);
+
             test.__default = true;
             this._addTest(test);
         }
@@ -261,12 +320,14 @@ WI.AuditManager = class AuditManager extends WI.Object
 };
 
 WI.AuditManager.RunningState = {
+    Disabled: "disabled",
     Inactive: "inactive",
     Active: "active",
     Stopping: "stopping",
 };
 
 WI.AuditManager.Event = {
+    EditingChanged: "audit-manager-editing-changed",
     TestAdded: "audit-manager-test-added",
     TestCompleted: "audit-manager-test-completed",
     TestRemoved: "audit-manager-test-removed",
index 04dfdbc..d41dafb 100644 (file)
 
 WI.AuditTestBase = class AuditTestBase extends WI.Object
 {
-    constructor(name, {description} = {})
+    constructor(name, {description, disabled} = {})
     {
         console.assert(typeof name === "string");
         console.assert(!description || typeof description === "string");
+        console.assert(disabled === undefined || typeof disabled === "boolean");
 
         super();
 
+        // This class should not be instantiated directly. Create a concrete subclass instead.
+        console.assert(this.constructor !== WI.AuditTestBase && this instanceof WI.AuditTestBase);
+
         this._name = name;
         this._description = description || null;
 
-        this._runningState = WI.AuditManager.RunningState.Inactive;
+        this._runningState = disabled ? WI.AuditManager.RunningState.Disabled : WI.AuditManager.RunningState.Inactive;
         this._result = null;
     }
 
@@ -46,10 +50,33 @@ WI.AuditTestBase = class AuditTestBase extends WI.Object
     get runningState() { return this._runningState; }
     get result() { return this._result; }
 
+    get disabled()
+    {
+        return this._runningState === WI.AuditManager.RunningState.Disabled;
+    }
+
+    set disabled(disabled)
+    {
+        console.assert(this._runningState === WI.AuditManager.RunningState.Disabled || this._runningState === WI.AuditManager.RunningState.Inactive);
+        if (this._runningState !== WI.AuditManager.RunningState.Disabled && this._runningState !== WI.AuditManager.RunningState.Inactive)
+            return;
+
+        let runningState = disabled ? WI.AuditManager.RunningState.Disabled : WI.AuditManager.RunningState.Inactive;
+        if (runningState === this._runningState)
+            return;
+
+        this._runningState = runningState;
+
+        this.dispatchEventToListeners(WI.AuditTestBase.Event.DisabledChanged);
+    }
+
     async start()
     {
         // Called from WI.AuditManager.
 
+        if (this.disabled)
+            return;
+
         console.assert(WI.auditManager.runningState === WI.AuditManager.RunningState.Active);
 
         console.assert(this._runningState === WI.AuditManager.RunningState.Inactive);
@@ -69,7 +96,10 @@ WI.AuditTestBase = class AuditTestBase extends WI.Object
     {
         // Called from WI.AuditManager.
 
-        console.assert(this._runningState !== WI.AuditManager.RunningState.Inactive);
+        if (this.disabled)
+            return;
+
+        console.assert(WI.auditManager.runningState === WI.AuditManager.RunningState.Stopping);
 
         if (this._runningState !== WI.AuditManager.RunningState.Active)
             return;
@@ -96,7 +126,7 @@ WI.AuditTestBase = class AuditTestBase extends WI.Object
         cookie["audit-" + this.constructor.TypeIdentifier + "-name"] = this._name;
     }
 
-    toJSON()
+    toJSON(key)
     {
         let json = {
             type: this.constructor.TypeIdentifier,
@@ -104,6 +134,8 @@ WI.AuditTestBase = class AuditTestBase extends WI.Object
         };
         if (this._description)
             json.description = this._description;
+        if (key === WI.ObjectStore.toJSONSymbol)
+            json.disabled = this.disabled;
         return json;
     }
 
@@ -117,6 +149,7 @@ WI.AuditTestBase = class AuditTestBase extends WI.Object
 
 WI.AuditTestBase.Event = {
     Completed: "audit-test-base-completed",
+    DisabledChanged: "audit-test-base-disabled-changed",
     Progress: "audit-test-base-progress",
     ResultCleared: "audit-test-base-result-cleared",
     Scheduled: "audit-test-base-scheduled",
index a6daef2..128b5b7 100644 (file)
 
 WI.AuditTestCase = class AuditTestCase extends WI.AuditTestBase
 {
-    constructor(name, test, {description} = {})
+    constructor(name, test, options = {})
     {
         console.assert(typeof test === "string");
 
-        super(name, {description});
+        super(name, options);
 
         this._test = test;
     }
@@ -41,7 +41,7 @@ WI.AuditTestCase = class AuditTestCase extends WI.AuditTestBase
         if (typeof payload !== "object" || payload === null)
             return null;
 
-        let {type, name, test, description} = payload;
+        let {type, name, test, description, disabled} = payload;
 
         if (type !== WI.AuditTestCase.TypeIdentifier)
             return null;
@@ -55,6 +55,8 @@ WI.AuditTestCase = class AuditTestCase extends WI.AuditTestBase
         let options = {};
         if (typeof description === "string")
             options.description = description;
+        if (typeof disabled === "boolean")
+            options.disabled = disabled;
 
         return new WI.AuditTestCase(name, test, options);
     }
@@ -63,9 +65,9 @@ WI.AuditTestCase = class AuditTestCase extends WI.AuditTestBase
 
     get test() { return this._test; }
 
-    toJSON()
+    toJSON(key)
     {
-        let json = super.toJSON();
+        let json = super.toJSON(key);
         json.test = this._test;
         return json;
     }
index e37ca63..2a16313 100644 (file)
 
 WI.AuditTestGroup = class AuditTestGroup extends WI.AuditTestBase
 {
-    constructor(name, tests, {description} = {})
+    constructor(name, tests, options = {})
     {
         console.assert(Array.isArray(tests));
 
-        super(name, {description});
+        // Set disabled once `_tests` is set so that it propagates.
+        let disabled = options.disabled;
+        options.disabled = false;
+
+        super(name, options);
 
         this._tests = tests;
+        this._preventDisabledPropagation = false;
+
+        if (disabled)
+            this.disabled = disabled;
 
         for (let test of this._tests) {
             test.addEventListener(WI.AuditTestBase.Event.Completed, this._handleTestCompleted, this);
+            test.addEventListener(WI.AuditTestBase.Event.DisabledChanged, this._handleTestDisabledChanged, this);
             test.addEventListener(WI.AuditTestBase.Event.Progress, this._handleTestProgress, this);
         }
     }
@@ -46,7 +55,7 @@ WI.AuditTestGroup = class AuditTestGroup extends WI.AuditTestBase
         if (typeof payload !== "object" || payload === null)
             return null;
 
-        let {type, name, tests, description} = payload;
+        let {type, name, tests, description, disabled} = payload;
 
         if (type !== WI.AuditTestGroup.TypeIdentifier)
             return null;
@@ -75,6 +84,8 @@ WI.AuditTestGroup = class AuditTestGroup extends WI.AuditTestBase
         let options = {};
         if (typeof description === "string")
             options.description = description;
+        if (typeof disabled === "boolean")
+            options.disabled = disabled;
 
         return new WI.AuditTestGroup(name, tests, options);
     }
@@ -83,6 +94,21 @@ WI.AuditTestGroup = class AuditTestGroup extends WI.AuditTestBase
 
     get tests() { return this._tests; }
 
+    get disabled()
+    {
+        return super.disabled;
+    }
+
+    set disabled(disabled)
+    {
+        if (!this._preventDisabledPropagation) {
+            for (let test of this._tests)
+                test.disabled = disabled;
+        }
+
+        super.disabled = disabled;
+    }
+
     stop()
     {
         // Called from WI.AuditManager.
@@ -107,10 +133,10 @@ WI.AuditTestGroup = class AuditTestGroup extends WI.AuditTestBase
         });
     }
 
-    toJSON()
+    toJSON(key)
     {
-        let json = super.toJSON();
-        json.tests = this._tests.map((testCase) => testCase.toJSON());
+        let json = super.toJSON(key);
+        json.tests = this._tests.map((testCase) => testCase.toJSON(key));
         return json;
     }
 
@@ -121,6 +147,8 @@ WI.AuditTestGroup = class AuditTestGroup extends WI.AuditTestBase
         let count = this._tests.length;
         for (let index = 0; index < count && this._runningState === WI.AuditManager.RunningState.Active; ++index) {
             let test = this._tests[index];
+            if (test.disabled)
+                continue;
 
             await test.start();
 
@@ -153,6 +181,21 @@ WI.AuditTestGroup = class AuditTestGroup extends WI.AuditTestBase
         this.dispatchEventToListeners(WI.AuditTestBase.Event.Completed);
     }
 
+    _handleTestDisabledChanged(event)
+    {
+        let enabledTestCount = this._tests.filter((test) => !test.disabled).length;
+        if (event.target.disabled && !enabledTestCount)
+            this.disabled = true;
+        else if (!event.target.disabled && enabledTestCount === 1) {
+            this._preventDisabledPropagation = true;
+            this.disabled = false;
+            this._preventDisabledPropagation = false;
+        } else {
+            // Don't change `disabled`, as we're currently in an "indeterminate" state.
+            this.dispatchEventToListeners(WI.AuditTestBase.Event.DisabledChanged);
+        }
+    }
+
     _handleTestProgress(event)
     {
         if (this._runningState !== WI.AuditManager.RunningState.Active)
@@ -161,6 +204,9 @@ WI.AuditTestGroup = class AuditTestGroup extends WI.AuditTestBase
         let walk = (tests) => {
             let count = 0;
             for (let test of tests) {
+                if (test.disabled)
+                    continue;
+
                 if (test instanceof WI.AuditTestCase)
                     ++count;
                 else if (test instanceof WI.AuditTestGroup)
@@ -170,8 +216,8 @@ WI.AuditTestGroup = class AuditTestGroup extends WI.AuditTestBase
         };
 
         this.dispatchEventToListeners(WI.AuditTestBase.Event.Progress, {
-            index: event.data.index + walk(this.tests.slice(0, this.tests.indexOf(event.target))),
-            count: walk(this.tests),
+            index: event.data.index + walk(this._tests.slice(0, this._tests.indexOf(event.target))),
+            count: walk(this._tests),
         });
     }
 };
index 9fa88d8..fcbdc51 100644 (file)
  */
 
 .sidebar > .panel.navigation.audit > .content {
+    display: flex;
+    flex-direction: column;
     top: var(--navigation-bar-height);
 }
 
+.sidebar > .panel.navigation.audit > .content > .tree-outline {
+    flex-grow: 1;
+}
+
+.sidebar > .panel.navigation.audit > .content .edit-audits:not(.disabled):active {
+    color: var(--glyph-color-pressed);
+}
+
+.sidebar > .panel.navigation.audit > .content .edit-audits:not(.disabled).activated {
+    color: var(--glyph-color-active);
+}
+
+.sidebar > .panel.navigation.audit > .content .edit-audits:not(.disabled).activated:active {
+    color: var(--glyph-color-active-pressed);
+}
+
+.sidebar > .panel.navigation.audit > .content .edit-audits.disabled {
+    color: var(--glyph-color-disabled);
+}
+
 .sidebar > .panel.navigation.audit.has-results:not(.has-tests) > .content > .message-text-view {
     position: initial;
     border-bottom: 1px solid var(--border-color);
@@ -39,3 +61,8 @@
 .sidebar > .panel.navigation.audit.has-results:not(.has-tests) > .content > .message-text-view > button {
     margin: 8px 0 7px;
 }
+
+.finish-editing-audits-placeholder.message-text-view .navigation-item-help .navigation-bar {
+    padding: 0;
+    vertical-align: 0.5px;
+}
index d723220..9c8fa83 100644 (file)
@@ -36,15 +36,29 @@ WI.AuditNavigationSidebarPanel = class AuditNavigationSidebarPanel extends WI.Na
     {
         let contentView = new WI.ContentView;
 
-        let contentPlaceholder = WI.createMessageTextView(WI.UIString("No audit selected"));
-        contentView.element.appendChild(contentPlaceholder);
-
-        let importNavigationItem = new WI.ButtonNavigationItem("import-audit", WI.UIString("Import"), "Images/Import.svg", 15, 15);
-        importNavigationItem.buttonStyle = WI.ButtonNavigationItem.Style.ImageAndText;
-        importNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._handleImportButtonNavigationItemClicked, this);
-
-        let importHelpElement = WI.createNavigationItemHelp(WI.UIString("Press %s to import a test or result file"), importNavigationItem);
-        contentPlaceholder.appendChild(importHelpElement);
+        if (WI.auditManager.editing) {
+            let contentPlaceholder = WI.createMessageTextView(WI.UIString("Editing audits"));
+            contentPlaceholder.classList.add("finish-editing-audits-placeholder");
+            contentView.element.appendChild(contentPlaceholder);
+
+            let finishEditingNavigationItem = new WI.ButtonNavigationItem("finish-editing-audits", WI.UIString("Done"));
+            finishEditingNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, (event) => {
+                WI.auditManager.editing = false;
+            });
+
+            let importHelpElement = WI.createNavigationItemHelp(WI.UIString("Press %s to stop editing"), finishEditingNavigationItem);
+            contentPlaceholder.appendChild(importHelpElement);
+        } else {
+            let contentPlaceholder = WI.createMessageTextView(WI.UIString("No audit selected"));
+            contentView.element.appendChild(contentPlaceholder);
+
+             let importNavigationItem = new WI.ButtonNavigationItem("import-audit", WI.UIString("Import"), "Images/Import.svg", 15, 15);
+            importNavigationItem.buttonStyle = WI.ButtonNavigationItem.Style.ImageAndText;
+            importNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._handleImportButtonNavigationItemClicked, this);
+
+             let importHelpElement = WI.createNavigationItemHelp(WI.UIString("Press %s to import a test or result file"), importNavigationItem);
+            contentPlaceholder.appendChild(importHelpElement);
+        }
 
         this.contentBrowser.showContentView(contentView);
     }
@@ -57,23 +71,31 @@ WI.AuditNavigationSidebarPanel = class AuditNavigationSidebarPanel extends WI.Na
 
         this.contentTreeOutline.allowsRepeatSelection = false;
 
-        let navigationBar = new WI.NavigationBar;
+        let controlsNavigationBar = new WI.NavigationBar;
 
         this._startStopButtonNavigationItem = new WI.ToggleButtonNavigationItem("audit-start-stop", WI.UIString("Start"), WI.UIString("Stop"), "Images/AuditStart.svg", "Images/AuditStop.svg", 13, 13);
         this._startStopButtonNavigationItem.buttonStyle = WI.ButtonNavigationItem.Style.ImageAndText;
         this._updateStartStopButtonNavigationItemState();
         this._startStopButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._handleStartStopButtonNavigationItemClicked, this);
-        navigationBar.addNavigationItem(this._startStopButtonNavigationItem);
+        controlsNavigationBar.addNavigationItem(this._startStopButtonNavigationItem);
 
-        navigationBar.addNavigationItem(new WI.DividerNavigationItem);
+        controlsNavigationBar.addNavigationItem(new WI.DividerNavigationItem);
 
         let importButtonNavigationItem = new WI.ButtonNavigationItem("audit-import", WI.UIString("Import"), "Images/Import.svg", 15, 15);
         importButtonNavigationItem.buttonStyle = WI.ButtonNavigationItem.Style.ImageAndText;
         importButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
         importButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._handleImportButtonNavigationItemClicked, this);
-        navigationBar.addNavigationItem(importButtonNavigationItem);
+        controlsNavigationBar.addNavigationItem(importButtonNavigationItem);
+
+        this.addSubview(controlsNavigationBar);
+
+        let editNavigationbar = new WI.NavigationBar;
+
+        this._editButtonNavigationItem = new WI.ActivateButtonNavigationItem("edit-audits", WI.UIString("Edit"), WI.UIString("Done"));
+        this._editButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._handleEditButtonNavigationItemClicked, this);
+        editNavigationbar.addNavigationItem(this._editButtonNavigationItem);
 
-        this.addSubview(navigationBar);
+        this.contentView.addSubview(editNavigationbar);
 
         for (let test of WI.auditManager.tests)
             this._addTest(test);
@@ -82,6 +104,7 @@ WI.AuditNavigationSidebarPanel = class AuditNavigationSidebarPanel extends WI.Na
             this._addResult(result, i);
         });
 
+        WI.auditManager.addEventListener(WI.AuditManager.Event.EditingChanged, this._handleAuditManagerEditingChanged, this);
         WI.auditManager.addEventListener(WI.AuditManager.Event.TestAdded, this._handleAuditTestAdded, this);
         WI.auditManager.addEventListener(WI.AuditManager.Event.TestCompleted, this._handleAuditTestCompleted, this);
         WI.auditManager.addEventListener(WI.AuditManager.Event.TestRemoved, this._handleAuditTestRemoved, this);
@@ -105,14 +128,30 @@ WI.AuditNavigationSidebarPanel = class AuditNavigationSidebarPanel extends WI.Na
             this._updateNoAuditsPlaceholder();
     }
 
+    hasCustomFilters()
+    {
+        return true;
+    }
+
+    matchTreeElementAgainstCustomFilters(treeElement, flags)
+    {
+        if (WI.auditManager.editing) {
+            if (treeElement.representedObject instanceof WI.AuditTestResultBase || treeElement.hasAncestor(this._resultsFolderTreeElement) || treeElement === this._resultsFolderTreeElement)
+                return false;
+        } else {
+            if (treeElement.representedObject instanceof WI.AuditTestBase && treeElement.representedObject.disabled)
+                return false;
+        }
+
+        return super.matchTreeElementAgainstCustomFilters(treeElement, flags);
+    }
+
     // Private
 
     _addTest(test)
     {
         this.element.classList.add("has-tests");
 
-        this._updateStartStopButtonNavigationItemState();
-
         let treeElement = new WI.AuditTreeElement(test);
 
         if (this._resultsFolderTreeElement) {
@@ -121,6 +160,9 @@ WI.AuditNavigationSidebarPanel = class AuditNavigationSidebarPanel extends WI.Na
         } else
             this.contentTreeOutline.appendChild(treeElement);
 
+        this._updateStartStopButtonNavigationItemState();
+        this._updateEditButtonNavigationItemState();
+
         this.hideEmptyContentPlaceholder();
     }
 
@@ -128,8 +170,6 @@ WI.AuditNavigationSidebarPanel = class AuditNavigationSidebarPanel extends WI.Na
     {
         this.element.classList.add("has-results");
 
-        this._updateStartStopButtonNavigationItemState();
-
         if (!this._resultsFolderTreeElement) {
             this._resultsFolderTreeElement = new WI.FolderTreeElement(WI.UIString("Results"));
             this.contentTreeOutline.appendChild(this._resultsFolderTreeElement);
@@ -144,14 +184,26 @@ WI.AuditNavigationSidebarPanel = class AuditNavigationSidebarPanel extends WI.Na
         }
         this._resultsFolderTreeElement.appendChild(resultFolderTreeElement);
 
+        console.assert(this._resultsFolderTreeElement.children.length === WI.auditManager.results.length);
+
         for (let resultItem of result)
             resultFolderTreeElement.appendChild(new WI.AuditTreeElement(resultItem));
+
+        this._updateStartStopButtonNavigationItemState();
+        this._updateEditButtonNavigationItemState();
     }
 
     _updateStartStopButtonNavigationItemState()
     {
-        this._startStopButtonNavigationItem.toggled = WI.auditManager.runningState !== WI.AuditManager.RunningState.Inactive;
-        this._startStopButtonNavigationItem.enabled = WI.auditManager.tests.length && WI.auditManager.runningState !== WI.AuditManager.RunningState.Stopping;
+        this._startStopButtonNavigationItem.toggled = WI.auditManager.runningState === WI.AuditManager.RunningState.Active || WI.auditManager.runningState === WI.AuditManager.RunningState.Stopping;
+        this._startStopButtonNavigationItem.enabled = WI.auditManager.tests.length && (WI.auditManager.runningState === WI.AuditManager.RunningState.Inactive || WI.auditManager.runningState === WI.AuditManager.RunningState.Active);
+    }
+
+     _updateEditButtonNavigationItemState()
+    {
+        this._editButtonNavigationItem.label = WI.auditManager.editing ? this._editButtonNavigationItem.activatedToolTip : this._editButtonNavigationItem.defaultToolTip;
+        this._editButtonNavigationItem.activated = WI.auditManager.editing;
+        this._editButtonNavigationItem.enabled = WI.auditManager.tests.length && (WI.auditManager.editing || WI.auditManager.runningState === WI.AuditManager.RunningState.Inactive);
     }
 
     _updateNoAuditsPlaceholder()
@@ -176,6 +228,30 @@ WI.AuditNavigationSidebarPanel = class AuditNavigationSidebarPanel extends WI.Na
             // be styled such that only the button is visible.
             this.contentView.element.insertBefore(contentPlaceholder, this.contentView.element.firstChild);
         }
+
+        this._updateEditButtonNavigationItemState();
+    }
+
+    _handleAuditManagerEditingChanged(event)
+    {
+        if (WI.auditManager.editing) {
+            console.assert(!this._selectedTreeElementBeforeEditing);
+            this._selectedTreeElementBeforeEditing = this.contentTreeOutline.selectedTreeElement;
+            if (this._selectedTreeElementBeforeEditing)
+                this._selectedTreeElementBeforeEditing.deselect();
+        } else if (this._selectedTreeElementBeforeEditing) {
+            if (!(this._selectedTreeElementBeforeEditing.representedObject instanceof WI.AuditTestBase) || !this._selectedTreeElementBeforeEditing.representedObject.disabled)
+                this._selectedTreeElementBeforeEditing.select();
+            this._selectedTreeElementBeforeEditing = null;
+        }
+
+        if (!this.contentTreeOutline.selectedTreeElement)
+            this.showDefaultContentView();
+
+        this._updateStartStopButtonNavigationItemState();
+        this._updateEditButtonNavigationItemState();
+
+        this.updateFilter();
     }
 
     _handleAuditTestAdded(event)
@@ -204,6 +280,7 @@ WI.AuditNavigationSidebarPanel = class AuditNavigationSidebarPanel extends WI.Na
     _handleAuditTestScheduled(event)
     {
         this._updateStartStopButtonNavigationItemState();
+        this._updateEditButtonNavigationItemState();
     }
 
     _treeSelectionDidChange(event)
@@ -217,6 +294,10 @@ WI.AuditNavigationSidebarPanel = class AuditNavigationSidebarPanel extends WI.Na
             return;
         }
 
+        console.assert(!WI.auditManager.editing);
+        if (WI.auditManager.editing)
+            return;
+
         let representedObject = treeElement.representedObject;
         if (representedObject instanceof WI.AuditTestCase || representedObject instanceof WI.AuditTestGroup
             || representedObject instanceof WI.AuditTestCaseResult || representedObject instanceof WI.AuditTestGroupResult) {
@@ -241,4 +322,9 @@ WI.AuditNavigationSidebarPanel = class AuditNavigationSidebarPanel extends WI.Na
     {
         WI.FileUtilities.importJSON((result) => WI.auditManager.processJSON(result));
     }
+
+    _handleEditButtonNavigationItemClicked(event)
+    {
+        WI.auditManager.editing = !WI.auditManager.editing;
+    }
 };
index 628eb1b..d5bbec5 100644 (file)
@@ -149,6 +149,9 @@ WI.AuditTestGroupContentView = class AuditTestGroupContentView extends WI.AuditT
         }
 
         for (let subobject of this._subobjects()) {
+            if (subobject instanceof WI.AuditTestBase && subobject.disabled)
+                continue;
+
             let view = WI.ContentView.contentViewForRepresentedObject(subobject);
             this.contentView.addSubview(view);
             view.shown();
index ddd2d95..6922f6a 100644 (file)
 }
 
 .tree-outline .item.audit > .status:not(:hover) > img.show-on-hover,
-.tree-outline .item.audit.test-group.expanded > .status:not(:hover) {
+.tree-outline .item.audit.test-group.expanded:not(.editing-audits) > .status:not(:hover) {
     opacity: 0;
 }
 
 .tree-outline .item.audit.manager-active > .status > img.show-on-hover,
-.tree-outline .item.audit.test-group.expanded > .status:hover > :not(img),
+.tree-outline .item.audit.test-group.expanded:not(.editing-audits) > .status:hover > :not(img),
 .tree-outline .item.audit.test-group-result.expanded > .status {
     display: none;
 }
index cd28ade..62f92b4 100644 (file)
@@ -61,6 +61,7 @@ WI.AuditTreeElement = class AuditTreeElement extends WI.GeneralTreeElement
         super.onattach();
 
         if (this.representedObject instanceof WI.AuditTestBase) {
+            this.representedObject.addEventListener(WI.AuditTestBase.Event.DisabledChanged, this._handleTestDisabledChanged, this);
             this.representedObject.addEventListener(WI.AuditTestBase.Event.ResultCleared, this._handleTestResultCleared, this);
 
             if (this.representedObject instanceof WI.AuditTestCase)
@@ -68,6 +69,7 @@ WI.AuditTreeElement = class AuditTreeElement extends WI.GeneralTreeElement
             else if (this.representedObject instanceof WI.AuditTestGroup)
                 this.representedObject.addEventListener(WI.AuditTestBase.Event.Scheduled, this._handleTestGroupScheduled, this);
 
+            WI.auditManager.addEventListener(WI.AuditManager.Event.EditingChanged, this._handleManagerEditingChanged, this);
             WI.auditManager.addEventListener(WI.AuditManager.Event.TestScheduled, this._handleAuditManagerTestScheduled, this);
             WI.auditManager.addEventListener(WI.AuditManager.Event.TestCompleted, this._handleAuditManagerTestCompleted, this);
         }
@@ -162,6 +164,11 @@ WI.AuditTreeElement = class AuditTreeElement extends WI.GeneralTreeElement
         super.populateContextMenu(contextMenu, event);
     }
 
+    canSelectOnMouseDown(event)
+    {
+        return !WI.auditManager.editing;
+    }
+
     // Private
 
     _start()
@@ -232,6 +239,14 @@ WI.AuditTreeElement = class AuditTreeElement extends WI.GeneralTreeElement
         this.status.value = progress || 0;
     }
 
+    _updateTestGroupDisabled()
+    {
+        this.status.checked = !this.representedObject.disabled;
+
+        if (this.representedObject instanceof WI.AuditTestGroup)
+            this.status.indeterminate = this.representedObject.tests.some((test) => test.disabled !== this.representedObject.tests[0].disabled);
+    }
+
     _handleTestCaseCompleted(event)
     {
         this.representedObject.removeEventListener(WI.AuditTestBase.Event.Completed, this._handleTestCaseCompleted, this);
@@ -239,6 +254,12 @@ WI.AuditTreeElement = class AuditTreeElement extends WI.GeneralTreeElement
         this._updateLevel();
     }
 
+    _handleTestDisabledChanged(event)
+    {
+        if (this.status instanceof HTMLInputElement && this.status.type === "checkbox")
+            this._updateTestGroupDisabled();
+    }
+
     _handleTestResultCleared(event)
     {
         this._updateLevel();
@@ -273,6 +294,24 @@ WI.AuditTreeElement = class AuditTreeElement extends WI.GeneralTreeElement
         this._showRunningProgress();
     }
 
+    _handleManagerEditingChanged(event)
+    {
+        if (WI.auditManager.editing) {
+            this.status = document.createElement("input");
+            this.status.type = "checkbox";
+            this._updateTestGroupDisabled();
+            this.status.addEventListener("change", () => {
+                this.representedObject.disabled = !this.representedObject.disabled;
+            });
+
+            this.addClassName("editing-audits");
+        } else {
+            this.removeClassName("editing-audits");
+
+            this._updateLevel();
+        }
+    }
+
     _handleAuditManagerTestScheduled(event)
     {
         this.addClassName("manager-active");
index 98925ca..2291459 100644 (file)
@@ -40,7 +40,7 @@
     display: none;
 }
 
-.tree-outline .children.expanded {
+.tree-outline .children.expanded:not([hidden]) {
     display: block;
 }