Web Inspector: Audit: create Audit Tab
authordrousso@apple.com <drousso@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 31 Oct 2018 01:11:36 +0000 (01:11 +0000)
committerdrousso@apple.com <drousso@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 31 Oct 2018 01:11:36 +0000 (01:11 +0000)
https://bugs.webkit.org/show_bug.cgi?id=190754

Reviewed by Matt Baker.

Source/WebInspectorUI:

Create an Audit tab for running audits on the inspected page. Leverage `Runtime.evaluate`
for running the audit tests (arbitrary JavaScript), and use the returned value to generate
a preview UI of the results. All tests/results can be exported/imported to formatted JSON:

`AuditTestCase` JSON:
    {
        "type": "test-case",
        "name": <string>,
        <optional> "description": <string>,
        "test": <stringified JavaScript function>,
    }

`AuditTestGroup` JSON:
    {
        "type": "test-group",
        "name": <string>,
        <optional> "description": <string>,
        "tests": [...<AuditTestCase, AuditTestGroup>],
    }

`AuditTestCaseResult` JSON:
    {
        "type": "test-case-result",
        "name": <string>,
        <optional> "description": <string>,
        "level": <"pass", "warn", "fail", "error", "unsupported">,
        "data": {
            "domNodes": [...<stringified CSS path>],
            "domAttributes": [...<string>],
            "errors": [...<string>],
        },
    }

`AuditTestGroupResult` JSON:
    {
        "type": "test-group-result",
        "name": <string>,
        <optional> "description": <string>,
        "results": [...<AuditTestCaseResult, AuditTestGroupResult>],
    }

More keys may be added in the future (especially for `AuditTestCaseResult.data`).

* UserInterface/Controllers/AuditManager.js:
(WI.AuditManager):
(WI.AuditManager.synthesizeError): Added.
(WI.AuditManager.prototype.get tests): Added.
(WI.AuditManager.prototype.get results): Added.
(WI.AuditManager.prototype.get runningState): Added.
(WI.AuditManager.prototype.start): Added.
(WI.AuditManager.prototype.stop): Added.
(WI.AuditManager.prototype.import): Added.
(WI.AuditManager.prototype.export): Added.
(WI.AuditManager.prototype._addTest): Added.
(WI.AuditManager.prototype._addResult): Added.
(WI.AuditManager.prototype.get testSuites): Deleted.
(WI.AuditManager.prototype.get reports): Deleted.
(WI.AuditManager.prototype.async runAuditTestByRepresentedObject): Deleted.
(WI.AuditManager.prototype.reportForId): Deleted.
(WI.AuditManager.prototype.removeAllReports): Deleted.
(WI.AuditManager.prototype.async _runTestCase): Deleted.

* UserInterface/Models/AuditTestBase.js: Added.
(WI.AuditTestBases):
(WI.AuditTestBases.prototype.get name):
(WI.AuditTestBases.prototype.get description):
(WI.AuditTestBases.prototype.get runningState):
(WI.AuditTestBases.prototype.get result):
(WI.AuditTestBases.prototype.async start):
(WI.AuditTestBases.prototype.stop):
(WI.AuditTestBases.prototype.clearResult):
(WI.AuditTestBases.prototype.saveIdentityToCookie):
(WI.AuditTestBases.prototype.toJSON):
(WI.AuditTestBases.prototype.async run):

* UserInterface/Models/AuditTestCase.js:
(WI.AuditTestCase):
(WI.AuditTestCase.fromPayload): Added.
(WI.AuditTestCase.prototype.toJSON): Added.
(WI.AuditTestCase.prototype.async run): Added.
(WI.AuditTestCase.prototype.async run.setLevel): Added.
(WI.AuditTestCase.prototype.async run.addError): Added.
(WI.AuditTestCase.prototype.async run.checkResultProperty.addErrorForValueType): Added.
(WI.AuditTestCase.prototype.async run.checkResultProperty): Added.
(WI.AuditTestCase.prototype.async run.async resultArrayForEach): Added.
(WI.AuditTestCase.prototype.get id): Deleted.
(WI.AuditTestCase.prototype.get name): Deleted.
(WI.AuditTestCase.prototype.get suite): Deleted.
(WI.AuditTestCase.prototype.get setup): Deleted.
(WI.AuditTestCase.prototype.get tearDown): Deleted.
(WI.AuditTestCase.prototype.get errorDetails): Deleted.

* UserInterface/Models/AuditTestGroup.js: Added.
(WI.AuditTestGroup):
(WI.AuditTestGroup.fromPayload):
(WI.AuditTestGroup.prototype.get tests):
(WI.AuditTestGroup.prototype.stop):
(WI.AuditTestGroup.prototype.clearResult):
(WI.AuditTestGroup.prototype.async run):
(WI.AuditTestGroup.prototype.toJSON):
(WI.AuditTestGroup.prototype._updateResult):
(WI.AuditTestGroup.prototype._handleTestCompleted):
(WI.AuditTestGroup.prototype._handleTestProgress):

* UserInterface/Models/AuditTestResultBase.js: Added.
(WI.AuditTestResultBase):
(WI.AuditTestResultBase.prototype.get name):
(WI.AuditTestResultBase.prototype.get description):
(WI.AuditTestResultBase.prototype.get result):
(WI.AuditTestResultBase.prototype.get didPass):
(WI.AuditTestResultBase.prototype.get didWarn):
(WI.AuditTestResultBase.prototype.get didFail):
(WI.AuditTestResultBase.prototype.get didError):
(WI.AuditTestResultBase.prototype.get unsupported):
(WI.AuditTestResultBase.prototype.saveIdentityToCookie):
(WI.AuditTestResultBase.prototype.toJSON):

* UserInterface/Models/AuditTestCaseResult.js: Added.
(WI.AuditTestCaseResult):
(WI.AuditTestCaseResult.fromPayload.checkArray):
(WI.AuditTestCaseResult.fromPayload):
(WI.AuditTestCaseResult.prototype.get level):
(WI.AuditTestCaseResult.prototype.get data):
(WI.AuditTestCaseResult.prototype.get didPass):
(WI.AuditTestCaseResult.prototype.get didWarn):
(WI.AuditTestCaseResult.prototype.get didFail):
(WI.AuditTestCaseResult.prototype.get didError):
(WI.AuditTestCaseResult.prototype.get unsupported):
(WI.AuditTestCaseResult.prototype.toJSON):

* UserInterface/Models/AuditTestGroupResult.js: Added.
(WI.AuditTestGroupResult):
(WI.AuditTestGroupResult.fromPayload):
(WI.AuditTestGroupResult.prototype.get results):
(WI.AuditTestGroupResult.prototype.get levelCounts):
(WI.AuditTestGroupResult.prototype.get didPass):
(WI.AuditTestGroupResult.prototype.get didWarn):
(WI.AuditTestGroupResult.prototype.get didFail):
(WI.AuditTestGroupResult.prototype.get didError):
(WI.AuditTestGroupResult.prototype.get unsupported):
(WI.AuditTestGroupResult.prototype.toJSON):

* UserInterface/Views/AuditTabContentView.js: Added.
(WI.AuditTabContentView):
(WI.AuditTabContentView.tabInfo):
(WI.AuditTabContentView.isTabAllowed):
(WI.AuditTabContentView.prototype.get type):
(WI.AuditTabContentView.prototype.get supportsSplitContentBrowser):
(WI.AuditTabContentView.prototype.canShowRepresentedObject):
(WI.AuditTabContentView.prototype.shown):
(WI.AuditTabContentView.prototype.hidden):
(WI.AuditTabContentView.prototype._handleSpace):

* UserInterface/Views/AuditNavigationSidebarPanel.js: Added.
(WI.AuditNavigationSidebarPanel):
(WI.AuditNavigationSidebarPanel.prototype.showDefaultContentView):
(WI.AuditNavigationSidebarPanel.prototype.initialLayout):
(WI.AuditNavigationSidebarPanel.prototype.closed):
(WI.AuditNavigationSidebarPanel.prototype._addTest):
(WI.AuditNavigationSidebarPanel.prototype._addResult):
(WI.AuditNavigationSidebarPanel.prototype._updateStartStopButtonNavigationItemState):
(WI.AuditNavigationSidebarPanel.prototype._handleAuditTestAdded):
(WI.AuditNavigationSidebarPanel.prototype._handleAuditTestCompleted):
(WI.AuditNavigationSidebarPanel.prototype._handleAuditTestScheduled):
(WI.AuditNavigationSidebarPanel.prototype._treeSelectionDidChange):
(WI.AuditNavigationSidebarPanel.prototype._handleStartStopButtonNavigationItemClicked):
(WI.AuditNavigationSidebarPanel.prototype._handleImportButtonNavigationItemClicked):
* UserInterface/Views/AuditNavigationSidebarPanel.css: Added.
(.sidebar > .panel.navigation.audit > .content):

* UserInterface/Views/AuditTreeElement.js: Added.
(WI.AuditTreeElement):
(WI.AuditTreeElement.prototype.get result):
(WI.AuditTreeElement.prototype.onattach):
(WI.AuditTreeElement.prototype.ondetach):
(WI.AuditTreeElement.prototype.onpopulate):
(WI.AuditTreeElement.prototype.populateContextMenu):
(WI.AuditTreeElement.prototype._start):
(WI.AuditTreeElement.prototype._updateLevel):
(WI.AuditTreeElement.prototype._showRunningSpinner):
(WI.AuditTreeElement.prototype._showRunningProgress):
(WI.AuditTreeElement.prototype._handleTestCaseCompleted):
(WI.AuditTreeElement.prototype._handleTestResultCleared):
(WI.AuditTreeElement.prototype._handleTestCaseScheduled):
(WI.AuditTreeElement.prototype._handleTestGroupCompleted):
(WI.AuditTreeElement.prototype._handleTestGroupProgress):
(WI.AuditTreeElement.prototype._handleTestGroupScheduled):
(WI.AuditTreeElement.prototype._handleStatusClick):
* UserInterface/Views/AuditTreeElement.css: Added.
(.tree-outline .item.audit > .status):
(.tree-outline .item.audit > .status > img):
(.tree-outline .item.audit:matches(.test-case, .test-group) > .status:hover > img):
(.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 > .status:hover > :not(img), .tree-outline .item.audit.test-group-result.expanded > .status):
(.tree-outline .item.audit > .status > img.pass):
(.tree-outline .item.audit > .status > img.warn):
(.tree-outline .item.audit > .status > img.fail):
(.tree-outline .item.audit > .status > img.error):
(.tree-outline .item.audit > .status > img.unsupported):
(.audit.test-case .icon):
(.audit.test-group .icon):
(.audit.test-case-result .icon):
(.audit.test-group-result .icon):

* UserInterface/Views/AuditTestContentView.js: Added.
(WI.AuditTestContentView):
(WI.AuditTestContentView.prototype.get navigationItems):
(WI.AuditTestContentView.prototype.get headerView):
(WI.AuditTestContentView.prototype.get contentView):
(WI.AuditTestContentView.prototype.get supportsSave):
(WI.AuditTestContentView.prototype.get saveData):
(WI.AuditTestContentView.prototype.initialLayout):
(WI.AuditTestContentView.prototype.layout):
(WI.AuditTestContentView.prototype.shown):
(WI.AuditTestContentView.prototype.hidden):
(WI.AuditTestContentView.prototype.get placeholderElement):
(WI.AuditTestContentView.prototype.set placeholderElement):
(WI.AuditTestContentView.prototype.showRunningPlaceholder):
(WI.AuditTestContentView.prototype.showStoppingPlaceholder):
(WI.AuditTestContentView.prototype.showNoResultPlaceholder):
(WI.AuditTestContentView.prototype.showNoResultDataPlaceholder):
(WI.AuditTestContentView.prototype.showFilteredPlaceholder):
(WI.AuditTestContentView.prototype.hidePlaceholder):
(WI.AuditTestContentView.prototype.applyFilter):
(WI.AuditTestContentView.prototype.resetFilter):
(WI.AuditTestContentView.prototype._exportAudit):
(WI.AuditTestContentView.prototype._updateExportButtonNavigationItemState):
(WI.AuditTestContentView.prototype._showPlaceholder):
(WI.AuditTestContentView.prototype._handleExportButtonNavigationItemClicked):
(WI.AuditTestContentView.prototype._handleTestChanged):
* UserInterface/Views/AuditTestContentView.css: Added.
(.content-view-container > .content-view.audit-test):
(.content-view-container > .content-view.audit-test > header):
(.content-view-container > .content-view.audit-test > header h1):
(.content-view-container > .content-view.audit-test > header p):
(.content-view.audit-test):
(.content-view.audit-test h1):
(.content-view.audit-test > header):
(.content-view.audit-test > header p):
(.content-view.audit-test .audit-test.filtered, .content-view.audit-test .audit-test .message-text-view):
(.content-view.audit-test > section):
(.content-view.audit-test > section > .message-text-view):
(.content-view.audit-test.showing-placeholder):
(.content-view.audit-test.showing-placeholder > section):
(.content-view.audit-test.showing-placeholder > section > :not(.message-text-view)):
(@media (prefers-dark-interface) .content-view.audit-test):

* UserInterface/Views/AuditTestCaseContentView.js: Added.
(WI.AuditTestCaseContentView):
(WI.AuditTestCaseContentView.prototype.initialLayout):
(WI.AuditTestCaseContentView.prototype.layout):
(WI.AuditTestCaseContentView.prototype.showRunningPlaceholder):
* UserInterface/Views/AuditTestCaseContentView.css: Added.
(.content-view-container > .content-view.audit-test-case > header):
(.content-view-container > .content-view.audit-test-case > section > :not(.message-text-view):first-child):
(.content-view.audit-test-case > header > h1):
(.content-view.audit-test-case > header > h1 > img):
(.content-view.audit-test-case > section > :not(.message-text-view)):
(.content-view.audit-test-case > section > :not(.message-text-view):last-child):
(.content-view.audit-test-case > section > :not(.message-text-view) + :not(.message-text-view)):
(.content-view.audit-test-case > section h1):
(.content-view.audit-test-case > section table):
(.content-view.audit-test-case > section table > tr + tr > td):
(.content-view.audit-test-case > section table > tr > td > :not(.tree-outline)):
(.content-view.audit-test-case > section table > tr > td:first-child):
(.content-view.audit-test-case > section > .dom-nodes > table > tr > td:first-child):
(.content-view.audit-test-case > section code):
(.content-view.audit-test-case > section mark):

* UserInterface/Views/AuditTestGroupContentView.js: Added.
(WI.AuditTestGroupContentView):
(WI.AuditTestGroupContentView.prototype.initialLayout):
(WI.AuditTestGroupContentView.prototype.layout):
(WI.AuditTestGroupContentView.prototype.shown):
(WI.AuditTestGroupContentView.prototype.hidden):
(WI.AuditTestGroupContentView.prototype.applyFilter):
(WI.AuditTestGroupContentView.prototype.resetFilter):
(WI.AuditTestGroupContentView.prototype.showRunningPlaceholder):
(WI.AuditTestGroupContentView.prototype._subobjects):
(WI.AuditTestGroupContentView.prototype._updateLevelScopeBar):
(WI.AuditTestGroupContentView.prototype._handleTestGroupCompleted):
(WI.AuditTestGroupContentView.prototype._handleTestGroupProgress):
(WI.AuditTestGroupContentView.prototype._handleTestGroupScheduled):
(WI.AuditTestGroupContentView.prototype._handleLevelScopeBarSelectionChanged):
* UserInterface/Views/AuditTestGroupContentView.css: Added.
(.content-view-container > .content-view.audit-test-group > header):
(.content-view.audit-test-group > header):
(.content-view.audit-test-group.no-matches + .audit-test-group > header):
(.content-view.audit-test-group > header, .content-view.audit-test-group:not(.filtered):last-child > header):
(.content-view.audit-test-group.contains-test-case > header):
(.content-view.audit-test-group.contains-test-case + .audit-test-group.contains-test-case):
(.content-view.audit-test-group.contains-test-case:not(.contains-test-group) > section, .content-view.audit-test-group.contains-test-case.contains-test-group > section > .audit-test-case):
(.content-view.audit-test-group > header > .information):
(.content-view.audit-test-group > header > .information > p):
(.content-view.audit-test-group > header > nav):
(.content-view.audit-test-group > header > nav:empty):
(.content-view.audit-test-group > header > nav:not(:empty):before):
(.content-view.audit-test-group > header > nav > .scope-bar > li):
(.content-view.audit-test-group > header > nav > .scope-bar > li:not(:hover, .selected)):
(.content-view.audit-test-group > header > nav > .scope-bar > li:last-child):
(.content-view.audit-test-group > header > nav > .scope-bar > li::before):
(.content-view.audit-test-group > header > nav > .scope-bar > li.pass::before):
(.content-view.audit-test-group > header > nav > .scope-bar > li.warn::before):
(.content-view.audit-test-group > header > nav > .scope-bar > li.fail::before):
(.content-view.audit-test-group > header > nav > .scope-bar > li.error::before):
(.content-view.audit-test-group > header > nav > .scope-bar > li.unsupported::before):
(.content-view.audit-test-group > header > .percentage-pass):
(.content-view.audit-test-group > header > .percentage-pass:not(:empty)::after):
(.content-view.audit-test-group > section > .audit-test-case:first-child, .content-view.audit-test-group > section > .audit-test-group + .audit-test-case, .content-view.audit-test-group > section > .audit-test-case + .audit-test-group):
(.content-view.audit-test-group > section > .audit-test-case:last-child):

* UserInterface/Views/ScopeBarItem.js:
(WI.ScopeBarItem):
(WI.ScopeBarItem.prototype.set selected):
* UserInterface/Views/MultipleScopeBarItem.js:
(WI.MultipleScopeBarItem.prototype.set selectedScopeBarItem):
Add an `independent` option that prevents selection changes from deselecting other
`WI.ScopeBarItem`s in the same `WI.ScopeBar` (`exclusive` takes precedence).

* UserInterface/Views/DOMTreeElement.js:
(WI.DOMTreeElement):
(WI.DOMTreeElement.prototype.highlightAttribute):
(WI.DOMTreeElement.prototype._buildAttributeDOM):
* UserInterface/Views/DOMTreeOutline.css:
(.tree-outline.dom li .highlight):

* UserInterface/Views/ToggleButtonNavigationItem.js:
(WI.ToggleButtonNavigationItem.prototype.set toggled):
Also change the `label` if the `ButtonStyle` has text.

* UserInterface/Base/Setting.js:
* UserInterface/Views/SettingsTabContentView.js:
(WI.SettingsTabContentView.prototype._createExperimentalSettingsView):

* UserInterface/Views/DividerNavigationItem.css:
(.navigation-bar .item.divider):

* UserInterface/Base/Utilities.js:
(Promise.chain): Added.

* UserInterface/Views/ContentView.js:
(WI.ContentView.createFromRepresentedObject):
(WI.ContentView.isViewable):

* UserInterface/Main.html:
* UserInterface/Base/Main.js:
(WI.loaded):
(WI.contentLoaded):

* UserInterface/Test.html:
* UserInterface/Base/Test.js:
(WI.loaded):

* UserInterface/Images/Audit.svg: Added.
* UserInterface/Images/AuditStart.svg: Added.
* UserInterface/Images/AuditStop.svg: Added.
* UserInterface/Images/AuditTestCase.svg: Added.
* UserInterface/Images/AuditTestCaseResult.svg: Added.
* UserInterface/Images/AuditTestError.svg: Added.
* UserInterface/Images/AuditTestFail.svg: Added.
* UserInterface/Images/AuditTestGroup.svg: Added.
* UserInterface/Images/AuditTestGroupResult.svg: Added.
* UserInterface/Images/AuditTestNoResult.svg: Added.
* UserInterface/Images/AuditTestPass.svg: Added.
* UserInterface/Images/AuditTestUnsupported.svg: Added.
* UserInterface/Images/AuditTestWarn.svg: Added.

* Localizations/en.lproj/localizedStrings.js:

LayoutTests:

* inspector/audit/resources/audit-utilities.js: Added.
* inspector/audit/basic-expected.txt: Added.
* inspector/audit/basic.html: Added.
* inspector/audit/data-domAttributes-expected.txt: Added.
* inspector/audit/data-domAttributes.html: Added.
* inspector/audit/data-domNodes-expected.txt: Added.
* inspector/audit/data-domNodes.html: Added.
* inspector/audit/data-errors-expected.txt: Added.
* inspector/audit/data-errors.html: Added.
* inspector/model/auditTestCase-expected.txt: Added.
* inspector/model/auditTestCase.html: Added.
* inspector/model/auditTestCaseResult-expected.txt: Added.
* inspector/model/auditTestCaseResult.html: Added.
* inspector/model/auditTestGroup-expected.txt: Added.
* inspector/model/auditTestGroup.html: Added.
* inspector/model/auditTestGroupResult-expected.txt: Added.
* inspector/model/auditTestGroupResult.html: Added.
* inspector/unit-tests/promise-utilities-expected.txt: Added.
* inspector/unit-tests/promise-utilities.html: Added.

* inspector/audit/audit-manager-expected.txt: Removed.
* inspector/audit/audit-manager.html: Removed.
* inspector/audit/audit-report-expected.txt: Removed.
* inspector/audit/audit-report.html: Removed.
* inspector/audit/audit-test-case-expected.txt: Removed.
* inspector/audit/audit-test-case.html: Removed.
* inspector/audit/audit-test-suite-expected.txt: Removed.
* inspector/audit/audit-test-suite.html: Removed.
* inspector/audit/resources/audit-test-fixtures.js: Removed.

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

77 files changed:
LayoutTests/ChangeLog
LayoutTests/inspector/audit/audit-manager-expected.txt [deleted file]
LayoutTests/inspector/audit/audit-manager.html [deleted file]
LayoutTests/inspector/audit/audit-report-expected.txt [deleted file]
LayoutTests/inspector/audit/audit-report.html [deleted file]
LayoutTests/inspector/audit/audit-test-case-expected.txt [deleted file]
LayoutTests/inspector/audit/audit-test-case.html [deleted file]
LayoutTests/inspector/audit/audit-test-suite-expected.txt [deleted file]
LayoutTests/inspector/audit/audit-test-suite.html [deleted file]
LayoutTests/inspector/audit/basic-expected.txt [new file with mode: 0644]
LayoutTests/inspector/audit/basic.html [new file with mode: 0644]
LayoutTests/inspector/audit/data-domAttributes-expected.txt [new file with mode: 0644]
LayoutTests/inspector/audit/data-domAttributes.html [new file with mode: 0644]
LayoutTests/inspector/audit/data-domNodes-expected.txt [new file with mode: 0644]
LayoutTests/inspector/audit/data-domNodes.html [new file with mode: 0644]
LayoutTests/inspector/audit/data-errors-expected.txt [new file with mode: 0644]
LayoutTests/inspector/audit/data-errors.html [new file with mode: 0644]
LayoutTests/inspector/audit/resources/audit-test-fixtures.js [deleted file]
LayoutTests/inspector/audit/resources/audit-utilities.js [new file with mode: 0644]
LayoutTests/inspector/model/auditTestCase-expected.txt [new file with mode: 0644]
LayoutTests/inspector/model/auditTestCase.html [new file with mode: 0644]
LayoutTests/inspector/model/auditTestCaseResult-expected.txt [new file with mode: 0644]
LayoutTests/inspector/model/auditTestCaseResult.html [new file with mode: 0644]
LayoutTests/inspector/model/auditTestGroup-expected.txt [new file with mode: 0644]
LayoutTests/inspector/model/auditTestGroup.html [new file with mode: 0644]
LayoutTests/inspector/model/auditTestGroupResult-expected.txt [new file with mode: 0644]
LayoutTests/inspector/model/auditTestGroupResult.html [new file with mode: 0644]
LayoutTests/inspector/unit-tests/promise-utilities-expected.txt [new file with mode: 0644]
LayoutTests/inspector/unit-tests/promise-utilities.html [new file with mode: 0644]
Source/WebInspectorUI/ChangeLog
Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js
Source/WebInspectorUI/UserInterface/Base/Main.js
Source/WebInspectorUI/UserInterface/Base/Setting.js
Source/WebInspectorUI/UserInterface/Base/Utilities.js
Source/WebInspectorUI/UserInterface/Controllers/AuditManager.js
Source/WebInspectorUI/UserInterface/Images/Audit.svg [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Images/AuditStart.svg [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Images/AuditStop.svg [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Images/AuditTestCase.svg [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Images/AuditTestCaseResult.svg [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Images/AuditTestError.svg [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Images/AuditTestFail.svg [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Images/AuditTestGroup.svg [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Images/AuditTestGroupResult.svg [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Images/AuditTestNoResult.svg [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Images/AuditTestPass.svg [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Images/AuditTestUnsupported.svg [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Images/AuditTestWarn.svg [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Main.html
Source/WebInspectorUI/UserInterface/Models/AuditReport.js [deleted file]
Source/WebInspectorUI/UserInterface/Models/AuditTestBase.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Models/AuditTestCase.js
Source/WebInspectorUI/UserInterface/Models/AuditTestCaseResult.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Models/AuditTestGroup.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Models/AuditTestGroupResult.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Models/AuditTestResultBase.js [moved from Source/WebInspectorUI/UserInterface/Models/AuditTestSuite.js with 54% similarity]
Source/WebInspectorUI/UserInterface/Test.html
Source/WebInspectorUI/UserInterface/Test/Test.js
Source/WebInspectorUI/UserInterface/Views/AuditNavigationSidebarPanel.css [moved from Source/WebInspectorUI/UserInterface/Models/AuditResult.js with 62% similarity]
Source/WebInspectorUI/UserInterface/Views/AuditNavigationSidebarPanel.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/AuditTabContentView.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/AuditTestCaseContentView.css [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/AuditTestCaseContentView.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/AuditTestContentView.css [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/AuditTestContentView.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/AuditTestGroupContentView.css [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/AuditTestGroupContentView.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/AuditTreeElement.css [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/AuditTreeElement.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/ContentView.js
Source/WebInspectorUI/UserInterface/Views/DOMTreeElement.js
Source/WebInspectorUI/UserInterface/Views/DOMTreeOutline.css
Source/WebInspectorUI/UserInterface/Views/DividerNavigationItem.css
Source/WebInspectorUI/UserInterface/Views/Main.css
Source/WebInspectorUI/UserInterface/Views/ScopeBarItem.js
Source/WebInspectorUI/UserInterface/Views/SettingsTabContentView.js
Source/WebInspectorUI/UserInterface/Views/ToggleButtonNavigationItem.js

index c128da3..eba87b0 100644 (file)
@@ -1,3 +1,40 @@
+2018-10-30  Devin Rousso  <drousso@apple.com>
+
+        Web Inspector: Audit: create Audit Tab
+        https://bugs.webkit.org/show_bug.cgi?id=190754
+
+        Reviewed by Matt Baker.
+
+        * inspector/audit/resources/audit-utilities.js: Added.
+        * inspector/audit/basic-expected.txt: Added.
+        * inspector/audit/basic.html: Added.
+        * inspector/audit/data-domAttributes-expected.txt: Added.
+        * inspector/audit/data-domAttributes.html: Added.
+        * inspector/audit/data-domNodes-expected.txt: Added.
+        * inspector/audit/data-domNodes.html: Added.
+        * inspector/audit/data-errors-expected.txt: Added.
+        * inspector/audit/data-errors.html: Added.
+        * inspector/model/auditTestCase-expected.txt: Added.
+        * inspector/model/auditTestCase.html: Added.
+        * inspector/model/auditTestCaseResult-expected.txt: Added.
+        * inspector/model/auditTestCaseResult.html: Added.
+        * inspector/model/auditTestGroup-expected.txt: Added.
+        * inspector/model/auditTestGroup.html: Added.
+        * inspector/model/auditTestGroupResult-expected.txt: Added.
+        * inspector/model/auditTestGroupResult.html: Added.
+        * inspector/unit-tests/promise-utilities-expected.txt: Added.
+        * inspector/unit-tests/promise-utilities.html: Added.
+
+        * inspector/audit/audit-manager-expected.txt: Removed.
+        * inspector/audit/audit-manager.html: Removed.
+        * inspector/audit/audit-report-expected.txt: Removed.
+        * inspector/audit/audit-report.html: Removed.
+        * inspector/audit/audit-test-case-expected.txt: Removed.
+        * inspector/audit/audit-test-case.html: Removed.
+        * inspector/audit/audit-test-suite-expected.txt: Removed.
+        * inspector/audit/audit-test-suite.html: Removed.
+        * inspector/audit/resources/audit-test-fixtures.js: Removed.
+
 2018-10-30  Dawei Fenton  <realdawei@apple.com>
 
         WebGL conformance: Failures and Timeouts in suite 2.0.0/conformance
diff --git a/LayoutTests/inspector/audit/audit-manager-expected.txt b/LayoutTests/inspector/audit/audit-manager-expected.txt
deleted file mode 100644 (file)
index a8ef603..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-Test for the AuditManager Instantiation.
-
-
-== Running test suite: AuditManager
--- Running test case: Adding an AuditTestSuite
-PASS: AuditManager should have 0 testSuite.
-Adding an AuditTestSuite to AuditManager.
-PASS: AuditManager should have 1 test suite.
-PASS: New test suite has the correct name.
-PASS: New test suite is of AuditTestSuite.
-
--- Running test case: Adding a duplicating AuditTestSuite
-PASS: Should produce an exception.
-Error: class testSuiteFixture1 already exists.
-
--- Running test case: Perform tests by AuditTestSuite.
-PASS: Receive a report that is of instance AuditReport.
-PASS: AuditReport is not writable
-PASS: There are two results in AuditReport.
-PASS: auditResults 0  is an instance of AuditResult.
-PASS: auditReport 0 is expected for test case fakeTest1.
-PASS: auditResults 1  is an instance of AuditResult.
-PASS: auditReport 1 is expected for test case fakeTest2.
-AuditReport is not writable.
-Attempting to add another AuditResult to AuditReport.
-PASS: AuditReport no longer accepts new AuditResults.
-PASS: Report represents the expected AuditTestSuite.
-
--- Running test case: Perform a test by AuditTestCase.
-PASS: Receive a report that is of instance AuditReport.
-PASS: AuditReport represents the expected AuditTestCase.
-
--- Running test case: AuditReports are unique.
-Only the most recent AuditReport for a case/suite is retained.
-PASS: The report represents the correct AuditTestSuite.
-
--- Running test case: Get AuditReport by AuditTestCase/Suite id.
-Running a test for an AuditTestSuite and an AuditTestCase.
-PASS: The report represents the correct AuditTestSuite.
-PASS: The report represents the correct AuditTestCase.
-
diff --git a/LayoutTests/inspector/audit/audit-manager.html b/LayoutTests/inspector/audit/audit-manager.html
deleted file mode 100644 (file)
index 5f07540..0000000
+++ /dev/null
@@ -1,139 +0,0 @@
-<!doctype html>
-<html>
-<head>
-<script src="../../http/tests/inspector/resources/inspector-test.js"></script>
-<script src="./resources/audit-test-fixtures.js"></script>
-<script>
-function test()
-{
-    let suite = InspectorTest.createAsyncSuite("AuditManager");
-
-    suite.addTestCase({
-        name: "Adding an AuditTestSuite",
-        description: "AuditManager should have one instantiated AuditTestSuite.",
-        async test(){
-            let auditManager = new WI.AuditManager;
-
-            InspectorTest.expectThat(!auditManager.testSuites.length, "AuditManager should have 0 testSuite.");
-
-            InspectorTest.log("Adding an AuditTestSuite to AuditManager.");
-            auditManager.addTestSuite(testSuiteFixture1);
-            let testSuite = auditManager.testSuites[0];
-
-            InspectorTest.expectEqual(auditManager.testSuites.length, 1, "AuditManager should have 1 test suite.");
-            InspectorTest.expectEqual(testSuite.name, "FakeTestSuite1", "New test suite has the correct name.");
-            InspectorTest.expectThat(testSuite instanceof WI.AuditTestSuite, "New test suite is of AuditTestSuite.");
-        }
-    });
-
-    suite.addTestCase({
-        name: "Adding a duplicating AuditTestSuite",
-        description: "Should throw exception for duplicated test suite.",
-        async test() {
-            let auditManager = new WI.AuditManager;
-
-            auditManager.addTestSuite(testSuiteFixture1);
-            auditManager.addTestSuite(testSuiteFixture2);
-
-            InspectorTest.expectException(() => {
-               auditManager.addTestSuite(testSuiteFixture1);
-            });
-        }
-    });
-
-    suite.addTestCase({
-        name: "Perform tests by AuditTestSuite.",
-        description: "Should produce report for AuditTestSuite.",
-        async test() {
-            let auditManager = new WI.AuditManager;
-
-            InspectorTest.assert(!auditManager._reports.size, "auditManager has no reports.");
-
-            let testSuite = new testSuiteFixture1;
-            let testCaseNames = testSuite.testCases.map(testCase => {
-                return testCase.name;
-            });
-            
-            await auditManager.runAuditTestByRepresentedObject(testSuite);
-
-            let auditReport = auditManager.reports[0];
-
-            InspectorTest.expectThat(auditReport instanceof WI.AuditReport, "Receive a report that is of instance AuditReport."); 
-            InspectorTest.expectThat(!auditReport._isWritable, "AuditReport is not writable");
-            InspectorTest.expectEqual(auditReport.resultsData.length, 2, "There are two results in AuditReport.");
-            
-            for (let i = 0; i < auditReport.resultsData.length; i++) {
-                let resultToTest = auditReport.resultsData[i];
-                InspectorTest.expectThat(resultToTest instanceof WI.AuditResult, `auditResults ${i}  is an instance of AuditResult.`)
-                InspectorTest.expectThat(testCaseNames.indexOf(resultToTest.name) >= 0, `auditReport ${i} is expected for test case ${resultToTest.name}.`);
-            }
-            InspectorTest.log("AuditReport is not writable.");
-            InspectorTest.log("Attempting to add another AuditResult to AuditReport.");
-            let additionalReport = auditReport.resultsData[1];
-            auditReport.addResult(additionalReport);
-
-            InspectorTest.expectEqual(auditReport.resultsData.length, 2, "AuditReport no longer accepts new AuditResults.");
-            InspectorTest.expectEqual(auditReport.representedTestSuite.id, testSuite.id, "Report represents the expected AuditTestSuite.");
-        }
-    });
-
-
-    suite.addTestCase({
-        name: "Perform a test by AuditTestCase.",
-        description: "Should produce report for AuditTestCase.",
-        async test() {
-            let auditManager = new WI.AuditManager;
-            auditManager.addTestSuite(testSuiteFixture1);
-            let testCase = auditManager.testSuites[0].testCases[0];
-
-            await auditManager.runAuditTestByRepresentedObject(testCase);
-
-            let auditReport = auditManager.reports[0];
-
-            InspectorTest.expectThat(auditReport instanceof WI.AuditReport, "Receive a report that is of instance AuditReport.");
-            InspectorTest.expectEqual(auditReport.representedTestCases[0], testCase, "AuditReport represents the expected AuditTestCase.");
-        }
-    });
-
-    suite.addTestCase({
-        name: "AuditReports are unique.",
-        description: "No AuditReport should represent the same AuditTestCase/Suite",
-        async test() {
-            let auditManager = new WI.AuditManager;
-            let testSuite = new testSuiteFixture1;
-
-            InspectorTest.log("Only the most recent AuditReport for a case/suite is retained.");
-
-            let results = [await auditManager.runAuditTestByRepresentedObject(testSuite), await auditManager.runAuditTestByRepresentedObject(testSuite)];
-
-            InspectorTest.expectEqual(results[0].representedTestSuite, results[1].representedTestSuite, "The report represents the correct AuditTestSuite.");
-        }
-    });
-
-    suite.addTestCase({
-        name: "Get AuditReport by AuditTestCase/Suite id.",
-        description: "Should return the correct AuditReport.",
-        async test() {
-            let auditManager = new WI.AuditManager;
-            let testSuite = new testSuiteFixture1;
-            let testCase = testSuite.testCases[0];
-
-            InspectorTest.log("Running a test for an AuditTestSuite and an AuditTestCase.");
-            let results = [await auditManager.runAuditTestByRepresentedObject(testSuite), await auditManager.runAuditTestByRepresentedObject(testCase)];
-
-            let auditReportForTestSuite = auditManager.reportForId(testSuite.id);
-            let auditReportForTestCase = auditManager.reportForId(testCase.id);
-
-            InspectorTest.expectEqual(results[0], auditReportForTestSuite, "The report represents the correct AuditTestSuite.");
-            InspectorTest.expectEqual(results[1], auditReportForTestCase, "The report represents the correct AuditTestCase.");
-        }
-    });
-
-    suite.runTestCasesAndFinish();
-}
-</script>
-</head>
-<body onload="runTest()">
-<p>Test for the AuditManager Instantiation.</p>
-</body>
-</html>
diff --git a/LayoutTests/inspector/audit/audit-report-expected.txt b/LayoutTests/inspector/audit/audit-report-expected.txt
deleted file mode 100644 (file)
index 6369257..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-Test for the AuditManager Instantiation.
-
-
-== Running test suite: AuditReport
--- Running test case: Instantiation with test suite
-PASS: Instantiate AuditReport with AuditTestSuite.
-
diff --git a/LayoutTests/inspector/audit/audit-report.html b/LayoutTests/inspector/audit/audit-report.html
deleted file mode 100644 (file)
index db122bd..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-<!doctype html>
-<html>
-<head>
-<script src="../../http/tests/inspector/resources/inspector-test.js"></script>
-<script src="resources/audit-test-fixtures.js"></script>
-<script>
-function test()
-{
-    let suite = InspectorTest.createAsyncSuite("AuditReport");
-
-    suite.addTestCase({
-        name: "Instantiation with test suite",
-        description: "should instantiate correctly.",
-        async test() {
-            let testSuite = new testSuiteFixture1;
-            InspectorTest.assert(testSuite instanceof WI.AuditTestSuite, "testSuite is AuditTestSuite.");
-            let report = new WI.AuditReport(testSuite);
-            InspectorTest.expectThat(report instanceof WI.AuditReport, "Instantiate AuditReport with AuditTestSuite.");
-        }
-    });
-
-    suite.runTestCasesAndFinish();
-}
-</script>
-</head>
-<body onload="runTest()">
-<p>Test for the AuditManager Instantiation.</p>
-</body>
-</html>
diff --git a/LayoutTests/inspector/audit/audit-test-case-expected.txt b/LayoutTests/inspector/audit/audit-test-case-expected.txt
deleted file mode 100644 (file)
index 9215ac4..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-Test for the AudtTestCase.
-
-
-== Running test suite: AuditTestCase
--- Running test case: Test functions must be asynchronous.
-PASS: Should produce an exception.
-Error: Test functions must be async functions.
-
diff --git a/LayoutTests/inspector/audit/audit-test-case.html b/LayoutTests/inspector/audit/audit-test-case.html
deleted file mode 100644 (file)
index 456d108..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-<!doctype html>
-<html>
-<head>
-<script src="../../http/tests/inspector/resources/inspector-test.js"></script>
-<script src="./resources/audit-test-fixtures.js"></script>
-<script>
-function test()
-{
-    let suite = InspectorTest.createAsyncSuite("AuditTestCase");
-
-    suite.addTestCase({
-        name: "Test functions must be asynchronous.",
-        description: "AuditTestCase should throw an exception when instantiated with a non-async function.",
-        async test() {
-            InspectorTest.expectException(() => {
-                new WI.AuditTestCase(new testSuiteFixture1, "fakeTest2", () => []);
-            });
-        }
-    })
-
-    suite.runTestCasesAndFinish();
-}
-</script>
-</head>
-<body onload="runTest()">
-<p>Test for the AudtTestCase.</p>
-</body>
-</html>
diff --git a/LayoutTests/inspector/audit/audit-test-suite-expected.txt b/LayoutTests/inspector/audit/audit-test-suite-expected.txt
deleted file mode 100644 (file)
index 8686f82..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-Test for the AuditTestSuite.
-
-
-== Running test suite: AuditTestSuite
--- Running test case: AuditTestSuite Id
-PASS: AuditTestSuite1 has ID with correct type.
-PASS: AuditTestSuite2 has ID with correct type.
-PASS: AuditTestSuites with same name have different unique IDs.
-
--- Running test case: AuditTestSuite testCaseCount
-PASS: There are two tests.
-
--- Running test case: AuditTestSuite should run tests sequentially.
-PASS: First test is ran.
-PASS: Second test is ran.
-PASS: Third test is ran.
-PASS: Fourth test is ran.
-PASS: Last test is ran.
-
diff --git a/LayoutTests/inspector/audit/audit-test-suite.html b/LayoutTests/inspector/audit/audit-test-suite.html
deleted file mode 100644 (file)
index 94c7f91..0000000
+++ /dev/null
@@ -1,106 +0,0 @@
-<!doctype html>
-<html>
-<head>
-<script src="../../http/tests/inspector/resources/inspector-test.js"></script>
-<script src="./resources/audit-test-fixtures.js"></script>
-<script>
-function test()
-{
-
-    let suite = InspectorTest.createAsyncSuite("AuditTestSuite");
-
-    let auditTestSuite1 = new testSuiteFixture1("FakeTestSuite", "FakeTestSuite");
-    let auditTestSuite2 = new testSuiteFixture1("FakeTestSuite", "FakeTestSuite");
-
-    suite.addTestCase({
-        name: "AuditTestSuite Id",
-        description: "Should exist and be unique",
-        async test() {
-            InspectorTest.expectEqual(typeof(auditTestSuite1.id), "symbol", "AuditTestSuite1 has ID with correct type.");
-            InspectorTest.expectEqual(typeof(auditTestSuite2.id), "symbol", "AuditTestSuite2 has ID with correct type.");
-            InspectorTest.expectThat(auditTestSuite1.id !== auditTestSuite2.id, "AuditTestSuites with same name have different unique IDs.");
-        }
-    });
-
-    suite.addTestCase({
-        name: "AuditTestSuite testCaseCount",
-        description: "Should represents correct number of test case.",
-        async test() {
-            InspectorTest.expectEqual(auditTestSuite1.testCases.length, 2, "There are two tests.");
-        }
-    });
-
-
-
-    suite.addTestCase({
-        name: "AuditTestSuite should run tests sequentially.",
-        description: "Tests should be ran in the order that was defined in the testDescriptor.",
-        async test() {
-                
-            let testOrderSymbol = Symbol("test-order");
-            window[testOrderSymbol] = 0;
-
-            let TestOrderSuite = class TestOrderSuite extends WI.AuditTestSuite 
-            {
-                static testCaseDescriptors()
-                {
-                    return [
-                        {
-                            name: "test 1",
-                            description: "order 1",
-                            async test() {
-                                InspectorTest.expectEqual(window[testOrderSymbol], 0, "First test is ran.");
-                                window[testOrderSymbol] = 1;
-                            }
-                        },
-                        {
-                            name: "test 2",
-                            description: "order 2",
-                            async test() {
-                                InspectorTest.expectEqual(window[testOrderSymbol], 1, "Second test is ran.");
-                                window[testOrderSymbol] = 2;
-                            }
-                        },
-                        {
-                            name: "test 3",
-                            description: "order 3",
-                            async test() {
-                                InspectorTest.expectEqual(window[testOrderSymbol], 2, "Third test is ran.");
-                                window[testOrderSymbol] = 3;
-                            }
-                        },
-                        {
-                            name: "test 4",
-                            description: "order 4",
-                            async test() {
-                                InspectorTest.expectEqual(window[testOrderSymbol], 3, "Fourth test is ran.");
-                                window[testOrderSymbol] = 4;
-                            }
-                        },
-                        {
-                            name: "test 4",
-                            description: "order 4",
-                            async test() {
-                                InspectorTest.expectEqual(window[testOrderSymbol], 4, "Last test is ran.");
-                            }
-                        }
-                    ]
-                }
-            }
-
-            let auditManager = new WI.AuditManager;
-            auditManager.addTestSuite(TestOrderSuite);
-
-            let result = auditManager.runAuditTestByRepresentedObject(auditManager.testSuites[0]);
-        }
-    });
-
-    suite.runTestCasesAndFinish();
-}
-</script>
-</head>
-<body onload="runTest()">
-<p>Test for the AuditTestSuite.</p>
-</body>
-</html>
diff --git a/LayoutTests/inspector/audit/basic-expected.txt b/LayoutTests/inspector/audit/basic-expected.txt
new file mode 100644 (file)
index 0000000..4246c29
--- /dev/null
@@ -0,0 +1,88 @@
+Testing the basic functionality of audits.
+
+
+== Running test suite: Audit.Basic
+-- Running test case: Audit.Basic.Boolean.True
+Testing value `true`...
+PASS: Result should be "pass".
+
+-- Running test case: Audit.Basic.Boolean.False
+Testing value `false`...
+PASS: Result should be "fail".
+
+-- Running test case: Audit.Basic.String.Pass
+Testing value `"pass"`...
+PASS: Result should be "pass".
+
+-- Running test case: Audit.Basic.String.Warn
+Testing value `"warn"`...
+PASS: Result should be "warn".
+
+-- Running test case: Audit.Basic.String.Fail
+Testing value `"fail"`...
+PASS: Result should be "fail".
+
+-- Running test case: Audit.Basic.String.Error
+Testing value `"error"`...
+PASS: Result should be "error".
+
+-- Running test case: Audit.Basic.String.Unsupported
+Testing value `"unsupported"`...
+PASS: Result should be "unsupported".
+
+-- Running test case: Audit.Basic.Object.Pass
+Testing value `{"level":"pass"}`...
+PASS: Result should be "pass".
+
+-- Running test case: Audit.Basic.Object.Warn
+Testing value `{"level":"warn"}`...
+PASS: Result should be "warn".
+
+-- Running test case: Audit.Basic.Object.Fail
+Testing value `{"level":"fail"}`...
+PASS: Result should be "fail".
+
+-- Running test case: Audit.Basic.Object.Error
+Testing value `{"level":"error"}`...
+PASS: Result should be "error".
+
+-- Running test case: Audit.Basic.Object.Unsupported
+Testing value `{"level":"unsupported"}`...
+PASS: Result should be "unsupported".
+
+-- Running test case: Audit.Basic.Error.Undefined
+Testing...
+PASS: Result should be "error".
+  errors:
+   - TypeError: eval(undefined) is not a function. (In 'eval(undefined)()', 'eval(undefined)' is undefined)
+
+-- Running test case: Audit.Basic.Error.Null
+Testing...
+PASS: Result should be "error".
+  errors:
+   - TypeError: eval(null) is not a function. (In 'eval(null)()', 'eval(null)' is null)
+
+-- Running test case: Audit.Basic.Error.Number
+Testing...
+PASS: Result should be "error".
+  errors:
+   - TypeError: eval(42) is not a function. (In 'eval(42)()', 'eval(42)' is 42)
+
+-- Running test case: Audit.Basic.Error.String
+Testing value `"foo"`...
+PASS: Result should be "error".
+  errors:
+   - Return string must be one of ["pass","warn","fail","error","unsupported"]
+
+-- Running test case: Audit.Basic.Error.Object
+Testing value `{}`...
+PASS: Result should be "error".
+  errors:
+   - Missing result level
+
+-- Running test case: Audit.Basic.Error.Variable
+Testing...
+PASS: Result should be "error".
+  errors:
+   - ReferenceError: Can't find variable: INVALID
+
diff --git a/LayoutTests/inspector/audit/basic.html b/LayoutTests/inspector/audit/basic.html
new file mode 100644 (file)
index 0000000..085a04a
--- /dev/null
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="../../http/tests/inspector/resources/inspector-test.js"></script>
+<script src="resources/audit-utilities.js"></script>
+<script>
+function test()
+{
+    let suite = InspectorTest.Audit.createSuite("Audit.Basic");
+
+    InspectorTest.Audit.addFunctionlessTest("Audit.Basic.Boolean.True", true, WI.AuditTestCaseResult.Level.Pass);
+    InspectorTest.Audit.addFunctionlessTest("Audit.Basic.Boolean.False", false, WI.AuditTestCaseResult.Level.Fail);
+
+    InspectorTest.Audit.addStringTest("Audit.Basic.String.Pass", WI.AuditTestCaseResult.Level.Pass, WI.AuditTestCaseResult.Level.Pass);
+    InspectorTest.Audit.addStringTest("Audit.Basic.String.Warn", WI.AuditTestCaseResult.Level.Warn, WI.AuditTestCaseResult.Level.Warn);
+    InspectorTest.Audit.addStringTest("Audit.Basic.String.Fail", WI.AuditTestCaseResult.Level.Fail, WI.AuditTestCaseResult.Level.Fail);
+    InspectorTest.Audit.addStringTest("Audit.Basic.String.Error", WI.AuditTestCaseResult.Level.Error, WI.AuditTestCaseResult.Level.Error);
+    InspectorTest.Audit.addStringTest("Audit.Basic.String.Unsupported", WI.AuditTestCaseResult.Level.Unsupported, WI.AuditTestCaseResult.Level.Unsupported);
+
+    InspectorTest.Audit.addObjectTest("Audit.Basic.Object.Pass", {level: WI.AuditTestCaseResult.Level.Pass}, WI.AuditTestCaseResult.Level.Pass);
+    InspectorTest.Audit.addObjectTest("Audit.Basic.Object.Warn", {level: WI.AuditTestCaseResult.Level.Warn}, WI.AuditTestCaseResult.Level.Warn);
+    InspectorTest.Audit.addObjectTest("Audit.Basic.Object.Fail", {level: WI.AuditTestCaseResult.Level.Fail}, WI.AuditTestCaseResult.Level.Fail);
+    InspectorTest.Audit.addObjectTest("Audit.Basic.Object.Error", {level: WI.AuditTestCaseResult.Level.Error}, WI.AuditTestCaseResult.Level.Error);
+    InspectorTest.Audit.addObjectTest("Audit.Basic.Object.Unsupported", {level: WI.AuditTestCaseResult.Level.Unsupported}, WI.AuditTestCaseResult.Level.Unsupported);
+
+    InspectorTest.Audit.addTest("Audit.Basic.Error.Undefined", undefined, WI.AuditTestCaseResult.Level.Error);
+    InspectorTest.Audit.addTest("Audit.Basic.Error.Null", null, WI.AuditTestCaseResult.Level.Error);
+    InspectorTest.Audit.addTest("Audit.Basic.Error.Number", 42, WI.AuditTestCaseResult.Level.Error);
+    InspectorTest.Audit.addStringTest("Audit.Basic.Error.String", "foo", WI.AuditTestCaseResult.Level.Error);
+    InspectorTest.Audit.addObjectTest("Audit.Basic.Error.Object", {}, WI.AuditTestCaseResult.Level.Error);
+    InspectorTest.Audit.addTest("Audit.Basic.Error.Variable", "INVALID", WI.AuditTestCaseResult.Level.Error);
+
+    suite.runTestCasesAndFinish();
+}
+</script>
+</head>
+<body onload="runTest()">
+    <p>Testing the basic functionality of audits.</p>
+</body>
+</html>
diff --git a/LayoutTests/inspector/audit/data-domAttributes-expected.txt b/LayoutTests/inspector/audit/data-domAttributes-expected.txt
new file mode 100644 (file)
index 0000000..552789b
--- /dev/null
@@ -0,0 +1,39 @@
+Testing audits involving DOM attributes.
+
+
+== Running test suite: Audit.DOMAttributes
+-- Running test case: Audit.DOMAttributes.Valid
+Testing value `{"level":"pass","domAttributes":["id","tabindex"]}`...
+PASS: Result should be "pass".
+  domAttributes:
+   - id
+   - tabindex
+
+-- Running test case: Audit.DOMAttributes.Undefined
+Testing value `{"level":"pass"}`...
+PASS: Result should be "pass".
+
+-- Running test case: Audit.DOMAttributes.Error.Null
+Testing value `{"level":"pass","domAttributes":null}`...
+PASS: Result should be "error".
+  errors:
+   - “domAttributes“ must be an array
+
+-- Running test case: Audit.DOMAttributes.Error.Number
+Testing value `{"level":"pass","domAttributes":42}`...
+PASS: Result should be "error".
+  errors:
+   - “domAttributes“ must be an array
+
+-- Running test case: Audit.DOMAttributes.Error.String
+Testing value `{"level":"pass","domAttributes":"foo"}`...
+PASS: Result should be "error".
+  errors:
+   - “domAttributes“ must be an array
+
+-- Running test case: Audit.DOMAttributes.Error.Object
+Testing value `{"level":"pass","domAttributes":{}}`...
+PASS: Result should be "error".
+  errors:
+   - “domAttributes“ must be an array
+
diff --git a/LayoutTests/inspector/audit/data-domAttributes.html b/LayoutTests/inspector/audit/data-domAttributes.html
new file mode 100644 (file)
index 0000000..814cf27
--- /dev/null
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="../../http/tests/inspector/resources/inspector-test.js"></script>
+<script src="resources/audit-utilities.js"></script>
+<script>
+function test()
+{
+    let suite = InspectorTest.Audit.createSuite("Audit.DOMAttributes");
+
+    InspectorTest.Audit.addObjectTest("Audit.DOMAttributes.Valid", {level: WI.AuditTestCaseResult.Level.Pass, domAttributes: ["id", "tabindex"]}, WI.AuditTestCaseResult.Level.Pass);
+
+    InspectorTest.Audit.addObjectTest("Audit.DOMAttributes.Undefined", {level: WI.AuditTestCaseResult.Level.Pass, domAttributes: undefined}, WI.AuditTestCaseResult.Level.Pass);
+
+    InspectorTest.Audit.addObjectTest("Audit.DOMAttributes.Error.Null", {level: WI.AuditTestCaseResult.Level.Pass, domAttributes: null}, WI.AuditTestCaseResult.Level.Error);
+    InspectorTest.Audit.addObjectTest("Audit.DOMAttributes.Error.Number", {level: WI.AuditTestCaseResult.Level.Pass, domAttributes: 42}, WI.AuditTestCaseResult.Level.Error);
+    InspectorTest.Audit.addObjectTest("Audit.DOMAttributes.Error.String", {level: WI.AuditTestCaseResult.Level.Pass, domAttributes: "foo"}, WI.AuditTestCaseResult.Level.Error);
+    InspectorTest.Audit.addObjectTest("Audit.DOMAttributes.Error.Object", {level: WI.AuditTestCaseResult.Level.Pass, domAttributes: {}}, WI.AuditTestCaseResult.Level.Error);
+
+    suite.runTestCasesAndFinish();
+}
+</script>
+</head>
+<body onload="runTest()">
+    <p>Testing audits involving DOM attributes.</p>
+</body>
+</html>
diff --git a/LayoutTests/inspector/audit/data-domNodes-expected.txt b/LayoutTests/inspector/audit/data-domNodes-expected.txt
new file mode 100644 (file)
index 0000000..05bb789
--- /dev/null
@@ -0,0 +1,57 @@
+Testing audits involving DOM nodes.
+
+
+== Running test suite: Audit.DOMNodes
+-- Running test case: Audit.DOMNodes.Tag
+Testing selector `div`...
+PASS: Result should be "fail".
+  domNodes:
+   - div#id1.class2.class3
+   - div#id2.class3.class1
+   - div#id3.class1.class2
+
+-- Running test case: Audit.DOMNodes.ID
+Testing selector `#id1`...
+PASS: Result should be "fail".
+  domNodes:
+   - div#id1.class2.class3
+
+-- Running test case: Audit.DOMNodes.ClassName
+Testing selector `.class1`...
+PASS: Result should be "fail".
+  domNodes:
+   - div#id2.class3.class1
+   - div#id3.class1.class2
+
+-- Running test case: Audit.DOMNodes.DoesNotExist
+Testing selector `DoesNotExist`...
+PASS: Result should be "pass".
+
+-- Running test case: Audit.DOMNodes.Undefined
+Testing value `{"level":"pass"}`...
+PASS: Result should be "pass".
+
+-- Running test case: Audit.DOMNodes.Error.Null
+Testing value `{"level":"pass","domNodes":null}`...
+PASS: Result should be "error".
+  errors:
+   - “domNodes“ must be an array
+
+-- Running test case: Audit.DOMNodes.Error.Number
+Testing value `{"level":"pass","domNodes":42}`...
+PASS: Result should be "error".
+  errors:
+   - “domNodes“ must be an array
+
+-- Running test case: Audit.DOMNodes.Error.String
+Testing value `{"level":"pass","domNodes":"foo"}`...
+PASS: Result should be "error".
+  errors:
+   - “domNodes“ must be an array
+
+-- Running test case: Audit.DOMNodes.Error.Object
+Testing value `{"level":"pass","domNodes":{}}`...
+PASS: Result should be "error".
+  errors:
+   - “domNodes“ must be an array
+
diff --git a/LayoutTests/inspector/audit/data-domNodes.html b/LayoutTests/inspector/audit/data-domNodes.html
new file mode 100644 (file)
index 0000000..f6d3ff1
--- /dev/null
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="../../http/tests/inspector/resources/inspector-test.js"></script>
+<script src="resources/audit-utilities.js"></script>
+<script>
+function test()
+{
+    let suite = InspectorTest.Audit.createSuite("Audit.DOMNodes");
+
+    InspectorTest.Audit.addDOMSelectorTest("Audit.DOMNodes.Tag", "div", WI.AuditTestCaseResult.Level.Fail);
+    InspectorTest.Audit.addDOMSelectorTest("Audit.DOMNodes.ID", "#id1", WI.AuditTestCaseResult.Level.Fail);
+    InspectorTest.Audit.addDOMSelectorTest("Audit.DOMNodes.ClassName", ".class1", WI.AuditTestCaseResult.Level.Fail);
+    InspectorTest.Audit.addDOMSelectorTest("Audit.DOMNodes.DoesNotExist", "DoesNotExist", WI.AuditTestCaseResult.Level.Pass);
+
+    InspectorTest.Audit.addObjectTest("Audit.DOMNodes.Undefined", {level: WI.AuditTestCaseResult.Level.Pass, domNodes: undefined}, WI.AuditTestCaseResult.Level.Pass);
+
+    InspectorTest.Audit.addObjectTest("Audit.DOMNodes.Error.Null", {level: WI.AuditTestCaseResult.Level.Pass, domNodes: null}, WI.AuditTestCaseResult.Level.Error);
+    InspectorTest.Audit.addObjectTest("Audit.DOMNodes.Error.Number", {level: WI.AuditTestCaseResult.Level.Pass, domNodes: 42}, WI.AuditTestCaseResult.Level.Error);
+    InspectorTest.Audit.addObjectTest("Audit.DOMNodes.Error.String", {level: WI.AuditTestCaseResult.Level.Pass, domNodes: "foo"}, WI.AuditTestCaseResult.Level.Error);
+    InspectorTest.Audit.addObjectTest("Audit.DOMNodes.Error.Object", {level: WI.AuditTestCaseResult.Level.Pass, domNodes: {}}, WI.AuditTestCaseResult.Level.Error);
+
+    suite.runTestCasesAndFinish();
+}
+</script>
+</head>
+<body onload="runTest()">
+    <p>Testing audits involving DOM nodes.</p>
+    <div id="id1" class="class2 class3"></div>
+    <div id="id2" class="class3 class1"></div>
+    <div id="id3" class="class1 class2"></div>
+</body>
+</html>
diff --git a/LayoutTests/inspector/audit/data-errors-expected.txt b/LayoutTests/inspector/audit/data-errors-expected.txt
new file mode 100644 (file)
index 0000000..a8409ec
--- /dev/null
@@ -0,0 +1,62 @@
+Testing audits involving DOM attributes.
+
+
+== Running test suite: Audit.Errors
+-- Running test case: Audit.Errors.MissingVariable
+Testing value `y`...
+PASS: Result should be "error".
+  errors:
+   - ReferenceError: Can't find variable: y
+
+-- Running test case: Audit.Errors.UndefinedAsObject
+Testing value `x.x.x`...
+PASS: Result should be "error".
+  errors:
+   - TypeError: undefined is not an object (evaluating 'x.x.x')
+
+-- Running test case: Audit.Errors.NotAFunction
+Testing value `x()`...
+PASS: Result should be "error".
+  errors:
+   - TypeError: x is not a function. (In 'x()', 'x' is an instance of Object)
+
+-- Running test case: Audit.Errors.InvalidLevel
+Testing value `{"level":"INVALID"}`...
+PASS: Result should be "error".
+  errors:
+   - Return string must be one of ["pass","warn","fail","error","unsupported"]
+
+-- Running test case: Audit.Errors.UserGenerated
+Testing value `{"level":"pass","errors":["user generated error"]}`...
+PASS: Result should be "error".
+  errors:
+   - All items in “errors“ must be error objects
+
+-- Running test case: Audit.Errors.Undefined
+Testing value `{"level":"pass"}`...
+PASS: Result should be "pass".
+
+-- Running test case: Audit.Errors.Error.Null
+Testing value `{"level":"pass","errors":null}`...
+PASS: Result should be "error".
+  errors:
+   - “errors“ must be an array
+
+-- Running test case: Audit.Errors.Error.Number
+Testing value `{"level":"pass","errors":42}`...
+PASS: Result should be "error".
+  errors:
+   - “errors“ must be an array
+
+-- Running test case: Audit.Errors.Error.String
+Testing value `{"level":"pass","errors":"foo"}`...
+PASS: Result should be "error".
+  errors:
+   - “errors“ must be an array
+
+-- Running test case: Audit.Errors.Error.Object
+Testing value `{"level":"pass","errors":{}}`...
+PASS: Result should be "error".
+  errors:
+   - “errors“ must be an array
+
diff --git a/LayoutTests/inspector/audit/data-errors.html b/LayoutTests/inspector/audit/data-errors.html
new file mode 100644 (file)
index 0000000..c2fe489
--- /dev/null
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="../../http/tests/inspector/resources/inspector-test.js"></script>
+<script src="resources/audit-utilities.js"></script>
+<script>
+
+let x = {};
+
+function test()
+{
+    let suite = InspectorTest.Audit.createSuite("Audit.Errors");
+
+    InspectorTest.Audit.addFunctionlessTest("Audit.Errors.MissingVariable", "y", WI.AuditTestCaseResult.Level.Error);
+    InspectorTest.Audit.addFunctionlessTest("Audit.Errors.UndefinedAsObject", "x.x.x", WI.AuditTestCaseResult.Level.Error);
+    InspectorTest.Audit.addFunctionlessTest("Audit.Errors.NotAFunction", "x()", WI.AuditTestCaseResult.Level.Error);
+    InspectorTest.Audit.addObjectTest("Audit.Errors.InvalidLevel", {level: "INVALID"}, WI.AuditTestCaseResult.Level.Error);
+    InspectorTest.Audit.addObjectTest("Audit.Errors.UserGenerated", {level: WI.AuditTestCaseResult.Level.Pass, errors: ["user generated error"]}, WI.AuditTestCaseResult.Level.Error);
+
+    InspectorTest.Audit.addObjectTest("Audit.Errors.Undefined", {level: WI.AuditTestCaseResult.Level.Pass, errors: undefined}, WI.AuditTestCaseResult.Level.Pass);
+
+    InspectorTest.Audit.addObjectTest("Audit.Errors.Error.Null", {level: WI.AuditTestCaseResult.Level.Pass, errors: null}, WI.AuditTestCaseResult.Level.Error);
+    InspectorTest.Audit.addObjectTest("Audit.Errors.Error.Number", {level: WI.AuditTestCaseResult.Level.Pass, errors: 42}, WI.AuditTestCaseResult.Level.Error);
+    InspectorTest.Audit.addObjectTest("Audit.Errors.Error.String", {level: WI.AuditTestCaseResult.Level.Pass, errors: "foo"}, WI.AuditTestCaseResult.Level.Error);
+    InspectorTest.Audit.addObjectTest("Audit.Errors.Error.Object", {level: WI.AuditTestCaseResult.Level.Pass, errors: {}}, WI.AuditTestCaseResult.Level.Error);
+
+    suite.runTestCasesAndFinish();
+}
+</script>
+</head>
+<body onload="runTest()">
+    <p>Testing audits involving DOM attributes.</p>
+    <div id="id1" class="class2 class3"></div>
+    <div id="id2" class="class3 class1"></div>
+    <div id="id3" class="class1 class2"></div>
+</body>
+</html>
diff --git a/LayoutTests/inspector/audit/resources/audit-test-fixtures.js b/LayoutTests/inspector/audit/resources/audit-test-fixtures.js
deleted file mode 100644 (file)
index c0c0535..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-TestPage.registerInitializer(() => {
-
-    window.testSuiteFixture1 = class testSuiteFixture1 extends WI.AuditTestSuite
-    {
-        constructor()
-        {
-            super("FakeTestSuite1", "FakeTestSuite1");
-        }
-
-        static testCaseDescriptors()
-        {
-            return [
-                {
-                    name: "fakeTest1",
-                    async test(){}
-                },
-                {
-                    name: "fakeTest2",
-                    async test()
-                    {
-                        throw new Error([1, 2, 3, 4]);
-                    }
-                }
-            ];
-        }
-    }
-
-    window.testSuiteFixture2 = class testSuiteFixture2 extends WI.AuditTestSuite
-    {
-        constructor()
-        {
-            super("FakeTestSuite2", "FakeTestSuite2");
-        }
-
-        static testCaseDescriptors()
-        {
-            return [
-                {
-                    name: "fakeTest2",
-                    async test()
-                    {
-                        return [];
-                    }
-                }
-            ];
-        }
-    }
-  
-});
diff --git a/LayoutTests/inspector/audit/resources/audit-utilities.js b/LayoutTests/inspector/audit/resources/audit-utilities.js
new file mode 100644 (file)
index 0000000..8e83017
--- /dev/null
@@ -0,0 +1,85 @@
+TestPage.registerInitializer(() => {
+    const querySelectorTest = `function() {
+    let domNodes = Array.from(document.querySelectorAll("%s"));
+    return {
+        level: domNodes.length ? "fail" : "pass",
+        domNodes,
+    };
+}`;
+
+    let suite = null;
+
+    function logArray(name, array) {
+        if (!array.length)
+            return;
+
+        InspectorTest.assert(array.every((item) => typeof item === "string"), name + "should only contain strings.");
+        InspectorTest.log("  " + name + ":");
+        for (let item of array)
+            InspectorTest.log("   - " + item);
+    }
+
+    InspectorTest.Audit = {};
+
+    InspectorTest.Audit.createSuite = function(name) {
+        suite = InspectorTest.createAsyncSuite(name);
+        return suite;
+    }
+
+    InspectorTest.Audit.addTest = function(name, test, level, logs = {}) {
+        suite.addTestCase({
+            name,
+            test(resolve, reject) {
+                let audit = new WI.AuditTestCase(name, test);
+
+                WI.auditManager.awaitEvent(WI.AuditManager.Event.TestCompleted).then((event) => {
+                    let results = WI.auditManager.results.lastValue;
+                    InspectorTest.assert(results.length === 1, "There should be 1 result.");
+
+                    let result = results[0];
+                    InspectorTest.assert(result instanceof WI.AuditTestCaseResult, "Result should be a WI.AuditTestCaseResult.");
+                    if (!result)
+                        return;
+
+                    InspectorTest.expectEqual(result.level, level, `Result should be "${level}".`);
+
+                    let data = result.data;
+                    if (data.domNodes) {
+                        InspectorTest.assert(data.domNodes.every((domNode) => domNode instanceof WI.DOMNode), "domNodes should only contain WI.DOMNode.");
+                        logArray("domNodes", data.domNodes.map((domNode) => domNode.displayName));
+                    }
+                    if (data.domAttributes)
+                        logArray("domAttributes", data.domAttributes);
+                    if (data.errors)
+                        logArray("errors", data.errors);
+                })
+                .then(resolve, reject);
+
+                InspectorTest.log("Testing" + (logs.beforeStart || "") + "...");
+
+                WI.auditManager.start([audit])
+                .then(resolve, reject);
+            },
+        });
+    };
+
+    InspectorTest.Audit.addFunctionlessTest = function(name, test, level) {
+        InspectorTest.Audit.addTest(name, `function() { return ${test} }`, level, {
+            beforeStart: ` value \`${test}\``,
+        });
+    };
+
+    InspectorTest.Audit.addStringTest = function(name, test, level) {
+        InspectorTest.Audit.addFunctionlessTest(name, `"${test}"`, level);
+    };
+
+    InspectorTest.Audit.addObjectTest = function(name, test, level) {
+        InspectorTest.Audit.addFunctionlessTest(name, JSON.stringify(test), level);
+    };
+
+    InspectorTest.Audit.addDOMSelectorTest = function(name, test, level) {
+        InspectorTest.Audit.addTest(name, querySelectorTest.format(test), level, {
+            beforeStart: ` selector \`${test}\``,
+        });
+    };
+});
diff --git a/LayoutTests/inspector/model/auditTestCase-expected.txt b/LayoutTests/inspector/model/auditTestCase-expected.txt
new file mode 100644 (file)
index 0000000..782db83
--- /dev/null
@@ -0,0 +1,38 @@
+Testing the functions of WI.AuditTestCase.
+
+
+== Running test suite: AuditTestCase
+-- Running test case: AuditTestCase.fromPayload.nullObject
+null
+
+-- Running test case: AuditTestCase.fromPayload.nonObject
+null
+
+-- Running test case: AuditTestCase.fromPayload.emptyObject
+null
+
+-- Running test case: AuditTestCase.fromPayload.invalidTopLevelMembers
+null
+
+-- Running test case: AuditTestCase.fromPayload.valid
+{
+  "type": "test-case",
+  "name": "valid test name",
+  "test": "function() { }"
+}
+
+-- Running test case: AuditTestCase.fromPayload.validWithInvalidOptionals
+{
+  "type": "test-case",
+  "name": "validWithInvalidOptionals test name",
+  "test": "validWithInvalidOptionals test function"
+}
+
+-- Running test case: AuditTestCase.fromPayload.validWithValidOptionals
+{
+  "type": "test-case",
+  "name": "validWithValidOptionals test name",
+  "description": "validWithValidOptionals test description",
+  "test": "validWithValidOptionals test function"
+}
+
diff --git a/LayoutTests/inspector/model/auditTestCase.html b/LayoutTests/inspector/model/auditTestCase.html
new file mode 100644 (file)
index 0000000..3dfc1ad
--- /dev/null
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="../../http/tests/inspector/resources/inspector-test.js"></script>
+<script>
+function test()
+{
+    let suite = InspectorTest.createAsyncSuite("AuditTestCase");
+
+    function addPayloadTest({name, payload}) {
+        suite.addTestCase({
+            name,
+            async test() {
+                let object = WI.AuditTestCase.fromPayload(payload);
+                InspectorTest.log(object ? JSON.stringify(object, null, 2) : object);
+            },
+        });
+    }
+
+    let payloadTests = [
+        {
+            name: "AuditTestCase.fromPayload.nullObject",
+            payload: null,
+        },
+        {
+            name: "AuditTestCase.fromPayload.nonObject",
+            payload: "INVALID",
+        },
+        {
+            name: "AuditTestCase.fromPayload.emptyObject",
+            payload: {},
+        },
+        {
+            name: "AuditTestCase.fromPayload.invalidTopLevelMembers",
+            payload: {
+                type: null,
+                name: null,
+                test: null,
+            },
+        },
+        {
+            name: "AuditTestCase.fromPayload.valid",
+            payload: {
+                type: WI.AuditTestCase.TypeIdentifier,
+                name: "valid test name",
+                test: "function() { }",
+            },
+        },
+        {
+            name: "AuditTestCase.fromPayload.validWithInvalidOptionals",
+            payload: {
+                type: WI.AuditTestCase.TypeIdentifier,
+                name: "validWithInvalidOptionals test name",
+                description: null,
+                test: "validWithInvalidOptionals test function",
+            },
+        },
+        {
+            name: "AuditTestCase.fromPayload.validWithValidOptionals",
+            payload: {
+                type: WI.AuditTestCase.TypeIdentifier,
+                name: "validWithValidOptionals test name",
+                description: "validWithValidOptionals test description",
+                test: "validWithValidOptionals test function",
+            },
+        },
+    ];
+    payloadTests.forEach(addPayloadTest);
+
+    suite.runTestCasesAndFinish();
+}
+</script>
+</head>
+<body onload="runTest()">
+    <p>Testing the functions of WI.AuditTestCase.</p>
+</body>
+</html>
diff --git a/LayoutTests/inspector/model/auditTestCaseResult-expected.txt b/LayoutTests/inspector/model/auditTestCaseResult-expected.txt
new file mode 100644 (file)
index 0000000..49a7388
--- /dev/null
@@ -0,0 +1,57 @@
+Testing the functions of WI.AuditTestCaseResult.
+
+
+== Running test suite: AuditTestCaseResult
+-- Running test case: AuditTestCaseResult.fromPayload.nullObject
+null
+
+-- Running test case: AuditTestCaseResult.fromPayload.nonObject
+null
+
+-- Running test case: AuditTestCaseResult.fromPayload.emptyObject
+null
+
+-- Running test case: AuditTestCaseResult.fromPayload.invalidTopLevelMembers
+null
+
+-- Running test case: AuditTestCaseResult.fromPayload.valid
+{
+  "type": "test-case-result",
+  "name": "valid test result name",
+  "level": "pass"
+}
+
+-- Running test case: AuditTestCaseResult.fromPayload.validWithInvalidOptionals
+{
+  "type": "test-case-result",
+  "name": "validWithInvalidOptionals test result name",
+  "level": "pass"
+}
+
+-- Running test case: AuditTestCaseResult.fromPayload.validWithInvalidSubOptionals
+{
+  "type": "test-case-result",
+  "name": "validWithInvalidSubOptionals test result name",
+  "description": "validWithInvalidSubOptionals test result description",
+  "level": "pass"
+}
+
+-- Running test case: AuditTestCaseResult.fromPayload.validWithValidSubOptionals
+{
+  "type": "test-case-result",
+  "name": "validWithValidSubOptionals test result name",
+  "description": "validWithValidSubOptionals test result description",
+  "level": "pass",
+  "data": {
+    "domNodes": [
+      "validWithValidSubOptionals test result domNode"
+    ],
+    "domAttributes": [
+      "validWithValidSubOptionals test result domAttribute"
+    ],
+    "errors": [
+      "validWithValidSubOptionals test result error"
+    ]
+  }
+}
+
diff --git a/LayoutTests/inspector/model/auditTestCaseResult.html b/LayoutTests/inspector/model/auditTestCaseResult.html
new file mode 100644 (file)
index 0000000..e4469ff
--- /dev/null
@@ -0,0 +1,97 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="../../http/tests/inspector/resources/inspector-test.js"></script>
+<script>
+function test()
+{
+    let suite = InspectorTest.createAsyncSuite("AuditTestCaseResult");
+
+    function addPayloadTest({name, payload}) {
+        suite.addTestCase({
+            name,
+            async test() {
+                let object = WI.AuditTestCaseResult.fromPayload(payload);
+                InspectorTest.log(object ? JSON.stringify(object, null, 2) : object);
+            },
+        });
+    }
+
+    let payloadTests = [
+        {
+            name: "AuditTestCaseResult.fromPayload.nullObject",
+            payload: null,
+        },
+        {
+            name: "AuditTestCaseResult.fromPayload.nonObject",
+            payload: "INVALID",
+        },
+        {
+            name: "AuditTestCaseResult.fromPayload.emptyObject",
+            payload: {},
+        },
+        {
+            name: "AuditTestCaseResult.fromPayload.invalidTopLevelMembers",
+            payload: {
+                type: null,
+                name: null,
+                level: null,
+            },
+        },
+        {
+            name: "AuditTestCaseResult.fromPayload.valid",
+            payload: {
+                type: WI.AuditTestCaseResult.TypeIdentifier,
+                name: "valid test result name",
+                level: WI.AuditTestCaseResult.Level.Pass,
+            },
+        },
+        {
+            name: "AuditTestCaseResult.fromPayload.validWithInvalidOptionals",
+            payload: {
+                type: WI.AuditTestCaseResult.TypeIdentifier,
+                name: "validWithInvalidOptionals test result name",
+                description: null,
+                level: WI.AuditTestCaseResult.Level.Pass,
+                data: null,
+            },
+        },
+        {
+            name: "AuditTestCaseResult.fromPayload.validWithInvalidSubOptionals",
+            payload: {
+                type: WI.AuditTestCaseResult.TypeIdentifier,
+                name: "validWithInvalidSubOptionals test result name",
+                description: "validWithInvalidSubOptionals test result description",
+                level: WI.AuditTestCaseResult.Level.Pass,
+                data: {
+                    domNodes: null,
+                    domAttributes: null,
+                    errors: null,
+                },
+            },
+        },
+        {
+            name: "AuditTestCaseResult.fromPayload.validWithValidSubOptionals",
+            payload: {
+                type: WI.AuditTestCaseResult.TypeIdentifier,
+                name: "validWithValidSubOptionals test result name",
+                description: "validWithValidSubOptionals test result description",
+                level: WI.AuditTestCaseResult.Level.Pass,
+                data: {
+                    domNodes: ["validWithValidSubOptionals test result domNode"],
+                    domAttributes: ["validWithValidSubOptionals test result domAttribute"],
+                    errors: ["validWithValidSubOptionals test result error"],
+                },
+            },
+        },
+    ];
+    payloadTests.forEach(addPayloadTest);
+
+    suite.runTestCasesAndFinish();
+}
+</script>
+</head>
+<body onload="runTest()">
+    <p>Testing the functions of WI.AuditTestCaseResult.</p>
+</body>
+</html>
diff --git a/LayoutTests/inspector/model/auditTestGroup-expected.txt b/LayoutTests/inspector/model/auditTestGroup-expected.txt
new file mode 100644 (file)
index 0000000..99b3935
--- /dev/null
@@ -0,0 +1,91 @@
+Testing the functions of WI.AuditTestGroup.
+
+
+== Running test suite: AuditTestGroup
+-- Running test case: AuditTestGroup.fromPayload.nullObject
+null
+
+-- Running test case: AuditTestGroup.fromPayload.nonObject
+null
+
+-- Running test case: AuditTestGroup.fromPayload.emptyObject
+null
+
+-- Running test case: AuditTestGroup.fromPayload.invalidTopLevelMembers
+null
+
+-- Running test case: AuditTestGroup.fromPayload.missingSubMembers
+null
+
+-- Running test case: AuditTestGroup.fromPayload.invalidSubMembers
+null
+
+-- Running test case: AuditTestGroup.fromPayload.valid
+{
+  "type": "test-group",
+  "name": "valid group name",
+  "tests": [
+    {
+      "type": "test-case",
+      "name": "valid test name",
+      "test": "valid test function"
+    }
+  ]
+}
+
+-- Running test case: AuditTestGroup.fromPayload.validWithInvalidOptionals
+{
+  "type": "test-group",
+  "name": "validWithInvalidOptionals group name",
+  "tests": [
+    {
+      "type": "test-case",
+      "name": "validWithInvalidOptionals test name",
+      "test": "validWithInvalidOptionals test function"
+    }
+  ]
+}
+
+-- Running test case: AuditTestGroup.fromPayload.validWithValidOptionals
+{
+  "type": "test-group",
+  "name": "validWithValidOptionals group name",
+  "description": "validWithValidOptionals group description",
+  "tests": [
+    {
+      "type": "test-case",
+      "name": "validWithValidOptionals test name",
+      "description": "validWithValidOptionals test description",
+      "test": "validWithValidOptionals test function"
+    }
+  ]
+}
+
+-- Running test case: AuditTestGroup.fromPayload.validNested
+{
+  "type": "test-group",
+  "name": "validNested group name",
+  "description": "validNested group description",
+  "tests": [
+    {
+      "type": "test-group",
+      "name": "validNested nested group name",
+      "description": "validNested nested group description",
+      "tests": [
+        {
+          "type": "test-case",
+          "name": "validNested nested test name",
+          "description": "validNested nested test description",
+          "test": "validNested nested test function"
+        }
+      ]
+    },
+    {
+      "type": "test-case",
+      "name": "validNested test name",
+      "description": "validNested test description",
+      "test": "validNested test function"
+    }
+  ]
+}
+
diff --git a/LayoutTests/inspector/model/auditTestGroup.html b/LayoutTests/inspector/model/auditTestGroup.html
new file mode 100644 (file)
index 0000000..ff0cd5e
--- /dev/null
@@ -0,0 +1,144 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="../../http/tests/inspector/resources/inspector-test.js"></script>
+<script>
+function test()
+{
+    let suite = InspectorTest.createAsyncSuite("AuditTestGroup");
+
+    function addPayloadTest({name, payload}) {
+        suite.addTestCase({
+            name,
+            async test() {
+                let object = WI.AuditTestGroup.fromPayload(payload);
+                InspectorTest.log(object ? JSON.stringify(object, null, 2) : object);
+            },
+        });
+    }
+
+    let payloadTests = [
+        {
+            name: "AuditTestGroup.fromPayload.nullObject",
+            payload: null,
+        },
+        {
+            name: "AuditTestGroup.fromPayload.nonObject",
+            payload: "INVALID",
+        },
+        {
+            name: "AuditTestGroup.fromPayload.emptyObject",
+            payload: {},
+        },
+        {
+            name: "AuditTestGroup.fromPayload.invalidTopLevelMembers",
+            payload: {
+                type: null,
+                name: null,
+                tests: null,
+            },
+        },
+        {
+            name: "AuditTestGroup.fromPayload.missingSubMembers",
+            payload: {
+                type: WI.AuditTestGroup.TypeIdentifier,
+                name: "missingSubMembers group name",
+                tests: [],
+            },
+        },
+        {
+            name: "AuditTestGroup.fromPayload.invalidSubMembers",
+            payload: {
+                type: WI.AuditTestGroup.TypeIdentifier,
+                name: "invalidSubMembers group name",
+                tests: [
+                    null,
+                ],
+            },
+        },
+        {
+            name: "AuditTestGroup.fromPayload.valid",
+            payload: {
+                type: WI.AuditTestGroup.TypeIdentifier,
+                name: "valid group name",
+                tests: [
+                    {
+                        type: WI.AuditTestCase.TypeIdentifier,
+                        name: "valid test name",
+                        test: "valid test function",
+                    },
+                ],
+            },
+        },
+        {
+            name: "AuditTestGroup.fromPayload.validWithInvalidOptionals",
+            payload: {
+                type: WI.AuditTestGroup.TypeIdentifier,
+                name: "validWithInvalidOptionals group name",
+                description: null,
+                tests: [
+                    {
+                        type: WI.AuditTestCase.TypeIdentifier,
+                        name: "validWithInvalidOptionals test name",
+                        description: null,
+                        test: "validWithInvalidOptionals test function",
+                    },
+                ],
+            },
+        },
+        {
+            name: "AuditTestGroup.fromPayload.validWithValidOptionals",
+            payload: {
+                type: WI.AuditTestGroup.TypeIdentifier,
+                name: "validWithValidOptionals group name",
+                description: "validWithValidOptionals group description",
+                tests: [
+                    {
+                        type: WI.AuditTestCase.TypeIdentifier,
+                        name: "validWithValidOptionals test name",
+                        description: "validWithValidOptionals test description",
+                        test: "validWithValidOptionals test function",
+                    },
+                ],
+            },
+        },
+        {
+            name: "AuditTestGroup.fromPayload.validNested",
+            payload: {
+                type: WI.AuditTestGroup.TypeIdentifier,
+                name: "validNested group name",
+                description: "validNested group description",
+                tests: [
+                    {
+                        type: WI.AuditTestGroup.TypeIdentifier,
+                        name: "validNested nested group name",
+                        description: "validNested nested group description",
+                        tests: [
+                            {
+                                type: WI.AuditTestCase.TypeIdentifier,
+                                name: "validNested nested test name",
+                                description: "validNested nested test description",
+                                test: "validNested nested test function",
+                            },
+                        ],
+                    },
+                    {
+                        type: WI.AuditTestCase.TypeIdentifier,
+                        name: "validNested test name",
+                        description: "validNested test description",
+                        test: "validNested test function",
+                    },
+                ],
+            },
+        },
+    ];
+    payloadTests.forEach(addPayloadTest);
+
+    suite.runTestCasesAndFinish();
+}
+</script>
+</head>
+<body onload="runTest()">
+    <p>Testing the functions of WI.AuditTestGroup.</p>
+</body>
+</html>
diff --git a/LayoutTests/inspector/model/auditTestGroupResult-expected.txt b/LayoutTests/inspector/model/auditTestGroupResult-expected.txt
new file mode 100644 (file)
index 0000000..27f5782
--- /dev/null
@@ -0,0 +1,124 @@
+Testing the functions of WI.AuditTestGroupResult.
+
+
+== Running test suite: AuditTestGroupResult
+-- Running test case: AuditTestGroupResult.fromPayload.nullObject
+null
+
+-- Running test case: AuditTestGroupResult.fromPayload.nonObject
+null
+
+-- Running test case: AuditTestGroupResult.fromPayload.emptyObject
+null
+
+-- Running test case: AuditTestGroupResult.fromPayload.invalidTopLevelMembers
+null
+
+-- Running test case: AuditTestGroupResult.fromPayload.missingSubMembers
+null
+
+-- Running test case: AuditTestGroupResult.fromPayload.invalidSubMembers
+null
+
+-- Running test case: AuditTestGroupResult.fromPayload.valid
+{
+  "type": "test-group-result",
+  "name": "valid group result name",
+  "results": [
+    {
+      "type": "test-case-result",
+      "name": "valid test result name",
+      "level": "pass"
+    }
+  ]
+}
+
+-- Running test case: AuditTestGroupResult.fromPayload.validWithInvalidOptionals
+{
+  "type": "test-group-result",
+  "name": "validWithInvalidOptionals group result name",
+  "results": [
+    {
+      "type": "test-case-result",
+      "name": "validWithInvalidOptionals test result name",
+      "level": "pass"
+    }
+  ]
+}
+
+-- Running test case: AuditTestGroupResult.fromPayload.validWithValidOptionals
+{
+  "type": "test-group-result",
+  "name": "validWithValidOptionals group result name",
+  "description": "validWithValidOptionals group result description",
+  "results": [
+    {
+      "type": "test-case-result",
+      "name": "validWithValidOptionals test result name",
+      "description": "validWithValidOptionals test result description",
+      "level": "pass",
+      "data": {
+        "domNodes": [
+          "validWithValidOptionals test result domNode"
+        ],
+        "domAttributes": [
+          "validWithValidOptionals test result domAttribute"
+        ],
+        "errors": [
+          "validWithValidOptionals test result error"
+        ]
+      }
+    }
+  ]
+}
+
+-- Running test case: AuditTestGroupResult.fromPayload.validNested
+{
+  "type": "test-group-result",
+  "name": "validNested group result name",
+  "description": "validNested group result description",
+  "results": [
+    {
+      "type": "test-group-result",
+      "name": "validNested nested group result name",
+      "description": "validNested nested group result description",
+      "results": [
+        {
+          "type": "test-case-result",
+          "name": "validNested nested test result name",
+          "description": "validNested nested test result description",
+          "level": "pass",
+          "data": {
+            "domNodes": [
+              "validNested nested test result domNode"
+            ],
+            "domAttributes": [
+              "validNested nested test result domAttribute"
+            ],
+            "errors": [
+              "validNested nested test result error"
+            ]
+          }
+        }
+      ]
+    },
+    {
+      "type": "test-case-result",
+      "name": "validNested test result name",
+      "description": "validNested test result description",
+      "level": "pass",
+      "data": {
+        "domNodes": [
+          "validNested test result domNode"
+        ],
+        "domAttributes": [
+          "validNested test result domAttribute"
+        ],
+        "errors": [
+          "validNested test result error"
+        ]
+      }
+    }
+  ]
+}
+
diff --git a/LayoutTests/inspector/model/auditTestGroupResult.html b/LayoutTests/inspector/model/auditTestGroupResult.html
new file mode 100644 (file)
index 0000000..f600354
--- /dev/null
@@ -0,0 +1,160 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="../../http/tests/inspector/resources/inspector-test.js"></script>
+<script>
+function test()
+{
+    let suite = InspectorTest.createAsyncSuite("AuditTestGroupResult");
+
+    function addPayloadTest({name, payload}) {
+        suite.addTestCase({
+            name,
+            async test() {
+                let object = WI.AuditTestGroupResult.fromPayload(payload);
+                InspectorTest.log(object ? JSON.stringify(object, null, 2) : object);
+            },
+        });
+    }
+
+    let payloadTests = [
+        {
+            name: "AuditTestGroupResult.fromPayload.nullObject",
+            payload: null,
+        },
+        {
+            name: "AuditTestGroupResult.fromPayload.nonObject",
+            payload: "INVALID",
+        },
+        {
+            name: "AuditTestGroupResult.fromPayload.emptyObject",
+            payload: {},
+        },
+        {
+            name: "AuditTestGroupResult.fromPayload.invalidTopLevelMembers",
+            payload: {
+                type: null,
+                name: null,
+                results: null,
+            },
+        },
+        {
+            name: "AuditTestGroupResult.fromPayload.missingSubMembers",
+            payload: {
+                type: WI.AuditTestGroupResult.TypeIdentifier,
+                name: "missingSubMembers group result name",
+                results: [],
+            },
+        },
+        {
+            name: "AuditTestGroupResult.fromPayload.invalidSubMembers",
+            payload: {
+                type: WI.AuditTestGroupResult.TypeIdentifier,
+                name: "invalidSubMembers group result name",
+                results: [
+                    null,
+                ],
+            },
+        },
+        {
+            name: "AuditTestGroupResult.fromPayload.valid",
+            payload: {
+                type: WI.AuditTestGroupResult.TypeIdentifier,
+                name: "valid group result name",
+                results: [
+                    {
+                        type: WI.AuditTestCaseResult.TypeIdentifier,
+                        name: "valid test result name",
+                        level: WI.AuditTestCaseResult.Level.Pass,
+                    },
+                ],
+            },
+        },
+        {
+            name: "AuditTestGroupResult.fromPayload.validWithInvalidOptionals",
+            payload: {
+                type: WI.AuditTestGroupResult.TypeIdentifier,
+                name: "validWithInvalidOptionals group result name",
+                description: null,
+                results: [
+                    {
+                        type: WI.AuditTestCaseResult.TypeIdentifier,
+                        name: "validWithInvalidOptionals test result name",
+                        description: null,
+                        level: WI.AuditTestCaseResult.Level.Pass,
+                        data: null,
+                    },
+                ],
+            },
+        },
+        {
+            name: "AuditTestGroupResult.fromPayload.validWithValidOptionals",
+            payload: {
+                type: WI.AuditTestGroupResult.TypeIdentifier,
+                name: "validWithValidOptionals group result name",
+                description: "validWithValidOptionals group result description",
+                results: [
+                    {
+                        type: WI.AuditTestCaseResult.TypeIdentifier,
+                        name: "validWithValidOptionals test result name",
+                        description: "validWithValidOptionals test result description",
+                        level: WI.AuditTestCaseResult.Level.Pass,
+                        data: {
+                            domNodes: ["validWithValidOptionals test result domNode"],
+                            domAttributes: ["validWithValidOptionals test result domAttribute"],
+                            errors: ["validWithValidOptionals test result error"],
+                        },
+                    },
+                ],
+            },
+        },
+        {
+            name: "AuditTestGroupResult.fromPayload.validNested",
+            payload: {
+                type: WI.AuditTestGroupResult.TypeIdentifier,
+                name: "validNested group result name",
+                description: "validNested group result description",
+                results: [
+                    {
+                        type: WI.AuditTestGroupResult.TypeIdentifier,
+                        name: "validNested nested group result name",
+                        description: "validNested nested group result description",
+                        results: [
+                            {
+                                type: WI.AuditTestCaseResult.TypeIdentifier,
+                                name: "validNested nested test result name",
+                                description: "validNested nested test result description",
+                                level: WI.AuditTestCaseResult.Level.Pass,
+                                data: {
+                                    domNodes: ["validNested nested test result domNode"],
+                                    domAttributes: ["validNested nested test result domAttribute"],
+                                    errors: ["validNested nested test result error"],
+                                },
+                            },
+                        ],
+                    },
+                    {
+                        type: WI.AuditTestCaseResult.TypeIdentifier,
+                        name: "validNested test result name",
+                        description: "validNested test result description",
+                        level: WI.AuditTestCaseResult.Level.Pass,
+                        data: {
+                            domNodes: ["validNested test result domNode"],
+                            domAttributes: ["validNested test result domAttribute"],
+                            errors: ["validNested test result error"],
+                        },
+                    },
+                ],
+            },
+        },
+    ];
+    payloadTests.forEach(addPayloadTest);
+
+    suite.runTestCasesAndFinish();
+}
+</script>
+</head>
+<body onload="runTest()">
+    <p>Testing the functions of WI.AuditTestGroupResult.</p>
+</body>
+</html>
diff --git a/LayoutTests/inspector/unit-tests/promise-utilities-expected.txt b/LayoutTests/inspector/unit-tests/promise-utilities-expected.txt
new file mode 100644 (file)
index 0000000..f5e5a6d
--- /dev/null
@@ -0,0 +1,7 @@
+
+== Running test suite: Promise
+-- Running test case: Promise.chain
+PASS: Resolved promise 0 with value 1
+PASS: Resolved promise 1 with value 2
+PASS: Resolved promise 2 with value 4
+
diff --git a/LayoutTests/inspector/unit-tests/promise-utilities.html b/LayoutTests/inspector/unit-tests/promise-utilities.html
new file mode 100644 (file)
index 0000000..2ba29a2
--- /dev/null
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="../../http/tests/inspector/resources/inspector-test.js"></script>
+<script>
+
+function test()
+{
+    let suite = InspectorTest.createAsyncSuite("Promise");
+
+    suite.addTestCase({
+        name: "Promise.chain",
+        async test() {
+            function createPromise(lastValue, i) {
+                return new Promise((resolve, reject) => {
+                    InspectorTest.pass(`Resolved promise ${i} with value ${lastValue}`);
+                    resolve(lastValue * 2);
+                });
+            }
+
+            let results = await Promise.chain([createPromise, createPromise, createPromise], 1);
+            InspectorTest.assert(results[0] === 2, "Result 1 should be 2 (1 * 2)");
+            InspectorTest.assert(results[1] === 4, "Result 2 should be 4 (1 * 2 * 2)");
+            InspectorTest.assert(results[2] === 8, "Result 3 should be 8 (1 * 2 * 2 * 2)");
+        }
+    });
+
+    suite.runTestCasesAndFinish();
+}
+</script>
+</head>
+<body onLoad="runTest()">
+</body>
+</html>
index 313d8cf..df4ef43 100644 (file)
@@ -1,5 +1,381 @@
 2018-10-30  Devin Rousso  <drousso@apple.com>
 
+        Web Inspector: Audit: create Audit Tab
+        https://bugs.webkit.org/show_bug.cgi?id=190754
+
+        Reviewed by Matt Baker.
+
+        Create an Audit tab for running audits on the inspected page. Leverage `Runtime.evaluate`
+        for running the audit tests (arbitrary JavaScript), and use the returned value to generate
+        a preview UI of the results. All tests/results can be exported/imported to formatted JSON:
+
+        `AuditTestCase` JSON:
+            {
+                "type": "test-case",
+                "name": <string>,
+                <optional> "description": <string>,
+                "test": <stringified JavaScript function>,
+            }
+
+        `AuditTestGroup` JSON:
+            {
+                "type": "test-group",
+                "name": <string>,
+                <optional> "description": <string>,
+                "tests": [...<AuditTestCase, AuditTestGroup>],
+            }
+
+        `AuditTestCaseResult` JSON:
+            {
+                "type": "test-case-result",
+                "name": <string>,
+                <optional> "description": <string>,
+                "level": <"pass", "warn", "fail", "error", "unsupported">,
+                "data": {
+                    "domNodes": [...<stringified CSS path>],
+                    "domAttributes": [...<string>],
+                    "errors": [...<string>],
+                },
+            }
+
+        `AuditTestGroupResult` JSON:
+            {
+                "type": "test-group-result",
+                "name": <string>,
+                <optional> "description": <string>,
+                "results": [...<AuditTestCaseResult, AuditTestGroupResult>],
+            }
+
+        More keys may be added in the future (especially for `AuditTestCaseResult.data`).
+
+        * UserInterface/Controllers/AuditManager.js:
+        (WI.AuditManager):
+        (WI.AuditManager.synthesizeError): Added.
+        (WI.AuditManager.prototype.get tests): Added.
+        (WI.AuditManager.prototype.get results): Added.
+        (WI.AuditManager.prototype.get runningState): Added.
+        (WI.AuditManager.prototype.start): Added.
+        (WI.AuditManager.prototype.stop): Added.
+        (WI.AuditManager.prototype.import): Added.
+        (WI.AuditManager.prototype.export): Added.
+        (WI.AuditManager.prototype._addTest): Added.
+        (WI.AuditManager.prototype._addResult): Added.
+        (WI.AuditManager.prototype.get testSuites): Deleted.
+        (WI.AuditManager.prototype.get reports): Deleted.
+        (WI.AuditManager.prototype.async runAuditTestByRepresentedObject): Deleted.
+        (WI.AuditManager.prototype.reportForId): Deleted.
+        (WI.AuditManager.prototype.removeAllReports): Deleted.
+        (WI.AuditManager.prototype.async _runTestCase): Deleted.
+
+        * UserInterface/Models/AuditTestBase.js: Added.
+        (WI.AuditTestBases):
+        (WI.AuditTestBases.prototype.get name):
+        (WI.AuditTestBases.prototype.get description):
+        (WI.AuditTestBases.prototype.get runningState):
+        (WI.AuditTestBases.prototype.get result):
+        (WI.AuditTestBases.prototype.async start):
+        (WI.AuditTestBases.prototype.stop):
+        (WI.AuditTestBases.prototype.clearResult):
+        (WI.AuditTestBases.prototype.saveIdentityToCookie):
+        (WI.AuditTestBases.prototype.toJSON):
+        (WI.AuditTestBases.prototype.async run):
+
+        * UserInterface/Models/AuditTestCase.js:
+        (WI.AuditTestCase):
+        (WI.AuditTestCase.fromPayload): Added.
+        (WI.AuditTestCase.prototype.toJSON): Added.
+        (WI.AuditTestCase.prototype.async run): Added.
+        (WI.AuditTestCase.prototype.async run.setLevel): Added.
+        (WI.AuditTestCase.prototype.async run.addError): Added.
+        (WI.AuditTestCase.prototype.async run.checkResultProperty.addErrorForValueType): Added.
+        (WI.AuditTestCase.prototype.async run.checkResultProperty): Added.
+        (WI.AuditTestCase.prototype.async run.async resultArrayForEach): Added.
+        (WI.AuditTestCase.prototype.get id): Deleted.
+        (WI.AuditTestCase.prototype.get name): Deleted.
+        (WI.AuditTestCase.prototype.get suite): Deleted.
+        (WI.AuditTestCase.prototype.get setup): Deleted.
+        (WI.AuditTestCase.prototype.get tearDown): Deleted.
+        (WI.AuditTestCase.prototype.get errorDetails): Deleted.
+
+        * UserInterface/Models/AuditTestGroup.js: Added.
+        (WI.AuditTestGroup):
+        (WI.AuditTestGroup.fromPayload):
+        (WI.AuditTestGroup.prototype.get tests):
+        (WI.AuditTestGroup.prototype.stop):
+        (WI.AuditTestGroup.prototype.clearResult):
+        (WI.AuditTestGroup.prototype.async run):
+        (WI.AuditTestGroup.prototype.toJSON):
+        (WI.AuditTestGroup.prototype._updateResult):
+        (WI.AuditTestGroup.prototype._handleTestCompleted):
+        (WI.AuditTestGroup.prototype._handleTestProgress):
+
+        * UserInterface/Models/AuditTestResultBase.js: Added.
+        (WI.AuditTestResultBase):
+        (WI.AuditTestResultBase.prototype.get name):
+        (WI.AuditTestResultBase.prototype.get description):
+        (WI.AuditTestResultBase.prototype.get result):
+        (WI.AuditTestResultBase.prototype.get didPass):
+        (WI.AuditTestResultBase.prototype.get didWarn):
+        (WI.AuditTestResultBase.prototype.get didFail):
+        (WI.AuditTestResultBase.prototype.get didError):
+        (WI.AuditTestResultBase.prototype.get unsupported):
+        (WI.AuditTestResultBase.prototype.saveIdentityToCookie):
+        (WI.AuditTestResultBase.prototype.toJSON):
+
+        * UserInterface/Models/AuditTestCaseResult.js: Added.
+        (WI.AuditTestCaseResult):
+        (WI.AuditTestCaseResult.fromPayload.checkArray):
+        (WI.AuditTestCaseResult.fromPayload):
+        (WI.AuditTestCaseResult.prototype.get level):
+        (WI.AuditTestCaseResult.prototype.get data):
+        (WI.AuditTestCaseResult.prototype.get didPass):
+        (WI.AuditTestCaseResult.prototype.get didWarn):
+        (WI.AuditTestCaseResult.prototype.get didFail):
+        (WI.AuditTestCaseResult.prototype.get didError):
+        (WI.AuditTestCaseResult.prototype.get unsupported):
+        (WI.AuditTestCaseResult.prototype.toJSON):
+
+        * UserInterface/Models/AuditTestGroupResult.js: Added.
+        (WI.AuditTestGroupResult):
+        (WI.AuditTestGroupResult.fromPayload):
+        (WI.AuditTestGroupResult.prototype.get results):
+        (WI.AuditTestGroupResult.prototype.get levelCounts):
+        (WI.AuditTestGroupResult.prototype.get didPass):
+        (WI.AuditTestGroupResult.prototype.get didWarn):
+        (WI.AuditTestGroupResult.prototype.get didFail):
+        (WI.AuditTestGroupResult.prototype.get didError):
+        (WI.AuditTestGroupResult.prototype.get unsupported):
+        (WI.AuditTestGroupResult.prototype.toJSON):
+
+        * UserInterface/Views/AuditTabContentView.js: Added.
+        (WI.AuditTabContentView):
+        (WI.AuditTabContentView.tabInfo):
+        (WI.AuditTabContentView.isTabAllowed):
+        (WI.AuditTabContentView.prototype.get type):
+        (WI.AuditTabContentView.prototype.get supportsSplitContentBrowser):
+        (WI.AuditTabContentView.prototype.canShowRepresentedObject):
+        (WI.AuditTabContentView.prototype.shown):
+        (WI.AuditTabContentView.prototype.hidden):
+        (WI.AuditTabContentView.prototype._handleSpace):
+
+        * UserInterface/Views/AuditNavigationSidebarPanel.js: Added.
+        (WI.AuditNavigationSidebarPanel):
+        (WI.AuditNavigationSidebarPanel.prototype.showDefaultContentView):
+        (WI.AuditNavigationSidebarPanel.prototype.initialLayout):
+        (WI.AuditNavigationSidebarPanel.prototype.closed):
+        (WI.AuditNavigationSidebarPanel.prototype._addTest):
+        (WI.AuditNavigationSidebarPanel.prototype._addResult):
+        (WI.AuditNavigationSidebarPanel.prototype._updateStartStopButtonNavigationItemState):
+        (WI.AuditNavigationSidebarPanel.prototype._handleAuditTestAdded):
+        (WI.AuditNavigationSidebarPanel.prototype._handleAuditTestCompleted):
+        (WI.AuditNavigationSidebarPanel.prototype._handleAuditTestScheduled):
+        (WI.AuditNavigationSidebarPanel.prototype._treeSelectionDidChange):
+        (WI.AuditNavigationSidebarPanel.prototype._handleStartStopButtonNavigationItemClicked):
+        (WI.AuditNavigationSidebarPanel.prototype._handleImportButtonNavigationItemClicked):
+        * UserInterface/Views/AuditNavigationSidebarPanel.css: Added.
+        (.sidebar > .panel.navigation.audit > .content):
+
+        * UserInterface/Views/AuditTreeElement.js: Added.
+        (WI.AuditTreeElement):
+        (WI.AuditTreeElement.prototype.get result):
+        (WI.AuditTreeElement.prototype.onattach):
+        (WI.AuditTreeElement.prototype.ondetach):
+        (WI.AuditTreeElement.prototype.onpopulate):
+        (WI.AuditTreeElement.prototype.populateContextMenu):
+        (WI.AuditTreeElement.prototype._start):
+        (WI.AuditTreeElement.prototype._updateLevel):
+        (WI.AuditTreeElement.prototype._showRunningSpinner):
+        (WI.AuditTreeElement.prototype._showRunningProgress):
+        (WI.AuditTreeElement.prototype._handleTestCaseCompleted):
+        (WI.AuditTreeElement.prototype._handleTestResultCleared):
+        (WI.AuditTreeElement.prototype._handleTestCaseScheduled):
+        (WI.AuditTreeElement.prototype._handleTestGroupCompleted):
+        (WI.AuditTreeElement.prototype._handleTestGroupProgress):
+        (WI.AuditTreeElement.prototype._handleTestGroupScheduled):
+        (WI.AuditTreeElement.prototype._handleStatusClick):
+        * UserInterface/Views/AuditTreeElement.css: Added.
+        (.tree-outline .item.audit > .status):
+        (.tree-outline .item.audit > .status > img):
+        (.tree-outline .item.audit:matches(.test-case, .test-group) > .status:hover > img):
+        (.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 > .status:hover > :not(img), .tree-outline .item.audit.test-group-result.expanded > .status):
+        (.tree-outline .item.audit > .status > img.pass):
+        (.tree-outline .item.audit > .status > img.warn):
+        (.tree-outline .item.audit > .status > img.fail):
+        (.tree-outline .item.audit > .status > img.error):
+        (.tree-outline .item.audit > .status > img.unsupported):
+        (.audit.test-case .icon):
+        (.audit.test-group .icon):
+        (.audit.test-case-result .icon):
+        (.audit.test-group-result .icon):
+
+        * UserInterface/Views/AuditTestContentView.js: Added.
+        (WI.AuditTestContentView):
+        (WI.AuditTestContentView.prototype.get navigationItems):
+        (WI.AuditTestContentView.prototype.get headerView):
+        (WI.AuditTestContentView.prototype.get contentView):
+        (WI.AuditTestContentView.prototype.get supportsSave):
+        (WI.AuditTestContentView.prototype.get saveData):
+        (WI.AuditTestContentView.prototype.initialLayout):
+        (WI.AuditTestContentView.prototype.layout):
+        (WI.AuditTestContentView.prototype.shown):
+        (WI.AuditTestContentView.prototype.hidden):
+        (WI.AuditTestContentView.prototype.get placeholderElement):
+        (WI.AuditTestContentView.prototype.set placeholderElement):
+        (WI.AuditTestContentView.prototype.showRunningPlaceholder):
+        (WI.AuditTestContentView.prototype.showStoppingPlaceholder):
+        (WI.AuditTestContentView.prototype.showNoResultPlaceholder):
+        (WI.AuditTestContentView.prototype.showNoResultDataPlaceholder):
+        (WI.AuditTestContentView.prototype.showFilteredPlaceholder):
+        (WI.AuditTestContentView.prototype.hidePlaceholder):
+        (WI.AuditTestContentView.prototype.applyFilter):
+        (WI.AuditTestContentView.prototype.resetFilter):
+        (WI.AuditTestContentView.prototype._exportAudit):
+        (WI.AuditTestContentView.prototype._updateExportButtonNavigationItemState):
+        (WI.AuditTestContentView.prototype._showPlaceholder):
+        (WI.AuditTestContentView.prototype._handleExportButtonNavigationItemClicked):
+        (WI.AuditTestContentView.prototype._handleTestChanged):
+        * UserInterface/Views/AuditTestContentView.css: Added.
+        (.content-view-container > .content-view.audit-test):
+        (.content-view-container > .content-view.audit-test > header):
+        (.content-view-container > .content-view.audit-test > header h1):
+        (.content-view-container > .content-view.audit-test > header p):
+        (.content-view.audit-test):
+        (.content-view.audit-test h1):
+        (.content-view.audit-test > header):
+        (.content-view.audit-test > header p):
+        (.content-view.audit-test .audit-test.filtered, .content-view.audit-test .audit-test .message-text-view):
+        (.content-view.audit-test > section):
+        (.content-view.audit-test > section > .message-text-view):
+        (.content-view.audit-test.showing-placeholder):
+        (.content-view.audit-test.showing-placeholder > section):
+        (.content-view.audit-test.showing-placeholder > section > :not(.message-text-view)):
+        (@media (prefers-dark-interface) .content-view.audit-test):
+
+        * UserInterface/Views/AuditTestCaseContentView.js: Added.
+        (WI.AuditTestCaseContentView):
+        (WI.AuditTestCaseContentView.prototype.initialLayout):
+        (WI.AuditTestCaseContentView.prototype.layout):
+        (WI.AuditTestCaseContentView.prototype.showRunningPlaceholder):
+        * UserInterface/Views/AuditTestCaseContentView.css: Added.
+        (.content-view-container > .content-view.audit-test-case > header):
+        (.content-view-container > .content-view.audit-test-case > section > :not(.message-text-view):first-child):
+        (.content-view.audit-test-case > header > h1):
+        (.content-view.audit-test-case > header > h1 > img):
+        (.content-view.audit-test-case > section > :not(.message-text-view)):
+        (.content-view.audit-test-case > section > :not(.message-text-view):last-child):
+        (.content-view.audit-test-case > section > :not(.message-text-view) + :not(.message-text-view)):
+        (.content-view.audit-test-case > section h1):
+        (.content-view.audit-test-case > section table):
+        (.content-view.audit-test-case > section table > tr + tr > td):
+        (.content-view.audit-test-case > section table > tr > td > :not(.tree-outline)):
+        (.content-view.audit-test-case > section table > tr > td:first-child):
+        (.content-view.audit-test-case > section > .dom-nodes > table > tr > td:first-child):
+        (.content-view.audit-test-case > section code):
+        (.content-view.audit-test-case > section mark):
+
+        * UserInterface/Views/AuditTestGroupContentView.js: Added.
+        (WI.AuditTestGroupContentView):
+        (WI.AuditTestGroupContentView.prototype.initialLayout):
+        (WI.AuditTestGroupContentView.prototype.layout):
+        (WI.AuditTestGroupContentView.prototype.shown):
+        (WI.AuditTestGroupContentView.prototype.hidden):
+        (WI.AuditTestGroupContentView.prototype.applyFilter):
+        (WI.AuditTestGroupContentView.prototype.resetFilter):
+        (WI.AuditTestGroupContentView.prototype.showRunningPlaceholder):
+        (WI.AuditTestGroupContentView.prototype._subobjects):
+        (WI.AuditTestGroupContentView.prototype._updateLevelScopeBar):
+        (WI.AuditTestGroupContentView.prototype._handleTestGroupCompleted):
+        (WI.AuditTestGroupContentView.prototype._handleTestGroupProgress):
+        (WI.AuditTestGroupContentView.prototype._handleTestGroupScheduled):
+        (WI.AuditTestGroupContentView.prototype._handleLevelScopeBarSelectionChanged):
+        * UserInterface/Views/AuditTestGroupContentView.css: Added.
+        (.content-view-container > .content-view.audit-test-group > header):
+        (.content-view.audit-test-group > header):
+        (.content-view.audit-test-group.no-matches + .audit-test-group > header):
+        (.content-view.audit-test-group > header, .content-view.audit-test-group:not(.filtered):last-child > header):
+        (.content-view.audit-test-group.contains-test-case > header):
+        (.content-view.audit-test-group.contains-test-case + .audit-test-group.contains-test-case):
+        (.content-view.audit-test-group.contains-test-case:not(.contains-test-group) > section, .content-view.audit-test-group.contains-test-case.contains-test-group > section > .audit-test-case):
+        (.content-view.audit-test-group > header > .information):
+        (.content-view.audit-test-group > header > .information > p):
+        (.content-view.audit-test-group > header > nav):
+        (.content-view.audit-test-group > header > nav:empty):
+        (.content-view.audit-test-group > header > nav:not(:empty):before):
+        (.content-view.audit-test-group > header > nav > .scope-bar > li):
+        (.content-view.audit-test-group > header > nav > .scope-bar > li:not(:hover, .selected)):
+        (.content-view.audit-test-group > header > nav > .scope-bar > li:last-child):
+        (.content-view.audit-test-group > header > nav > .scope-bar > li::before):
+        (.content-view.audit-test-group > header > nav > .scope-bar > li.pass::before):
+        (.content-view.audit-test-group > header > nav > .scope-bar > li.warn::before):
+        (.content-view.audit-test-group > header > nav > .scope-bar > li.fail::before):
+        (.content-view.audit-test-group > header > nav > .scope-bar > li.error::before):
+        (.content-view.audit-test-group > header > nav > .scope-bar > li.unsupported::before):
+        (.content-view.audit-test-group > header > .percentage-pass):
+        (.content-view.audit-test-group > header > .percentage-pass:not(:empty)::after):
+        (.content-view.audit-test-group > section > .audit-test-case:first-child, .content-view.audit-test-group > section > .audit-test-group + .audit-test-case, .content-view.audit-test-group > section > .audit-test-case + .audit-test-group):
+        (.content-view.audit-test-group > section > .audit-test-case:last-child):
+
+        * UserInterface/Views/ScopeBarItem.js:
+        (WI.ScopeBarItem):
+        (WI.ScopeBarItem.prototype.set selected):
+        * UserInterface/Views/MultipleScopeBarItem.js:
+        (WI.MultipleScopeBarItem.prototype.set selectedScopeBarItem):
+        Add an `independent` option that prevents selection changes from deselecting other
+        `WI.ScopeBarItem`s in the same `WI.ScopeBar` (`exclusive` takes precedence).
+
+        * UserInterface/Views/DOMTreeElement.js:
+        (WI.DOMTreeElement):
+        (WI.DOMTreeElement.prototype.highlightAttribute):
+        (WI.DOMTreeElement.prototype._buildAttributeDOM):
+        * UserInterface/Views/DOMTreeOutline.css:
+        (.tree-outline.dom li .highlight):
+
+        * UserInterface/Views/ToggleButtonNavigationItem.js:
+        (WI.ToggleButtonNavigationItem.prototype.set toggled):
+        Also change the `label` if the `ButtonStyle` has text.
+
+        * UserInterface/Base/Setting.js:
+        * UserInterface/Views/SettingsTabContentView.js:
+        (WI.SettingsTabContentView.prototype._createExperimentalSettingsView):
+
+        * UserInterface/Views/DividerNavigationItem.css:
+        (.navigation-bar .item.divider):
+
+        * UserInterface/Base/Utilities.js:
+        (Promise.chain): Added.
+
+        * UserInterface/Views/ContentView.js:
+        (WI.ContentView.createFromRepresentedObject):
+        (WI.ContentView.isViewable):
+
+        * UserInterface/Main.html:
+        * UserInterface/Base/Main.js:
+        (WI.loaded):
+        (WI.contentLoaded):
+
+        * UserInterface/Test.html:
+        * UserInterface/Base/Test.js:
+        (WI.loaded):
+
+        * UserInterface/Images/Audit.svg: Added.
+        * UserInterface/Images/AuditStart.svg: Added.
+        * UserInterface/Images/AuditStop.svg: Added.
+        * UserInterface/Images/AuditTestCase.svg: Added.
+        * UserInterface/Images/AuditTestCaseResult.svg: Added.
+        * UserInterface/Images/AuditTestError.svg: Added.
+        * UserInterface/Images/AuditTestFail.svg: Added.
+        * UserInterface/Images/AuditTestGroup.svg: Added.
+        * UserInterface/Images/AuditTestGroupResult.svg: Added.
+        * UserInterface/Images/AuditTestNoResult.svg: Added.
+        * UserInterface/Images/AuditTestPass.svg: Added.
+        * UserInterface/Images/AuditTestUnsupported.svg: Added.
+        * UserInterface/Images/AuditTestWarn.svg: Added.
+
+        * Localizations/en.lproj/localizedStrings.js:
+
+2018-10-30  Devin Rousso  <drousso@apple.com>
+
         Web Inspector: provide options to WI.cssPath for more verbosity
         https://bugs.webkit.org/show_bug.cgi?id=190987
 
index 359cfdc..d7df51a 100644 (file)
@@ -22,12 +22,17 @@ localizedStrings["%.2f\u00d7"] = "%.2f\u00d7";
 localizedStrings["%.2fms"] = "%.2fms";
 localizedStrings["%.2fs"] = "%.2fs";
 localizedStrings["%.3fms"] = "%.3fms";
+localizedStrings["%d Error"] = "%d Error";
 localizedStrings["%d Errors"] = "%d Errors";
 localizedStrings["%d Errors, %d Warnings"] = "%d Errors, %d Warnings";
+localizedStrings["%d Fail"] = "%d Fail";
 localizedStrings["%d Frame"] = "%d Frame";
 localizedStrings["%d Frames"] = "%d Frames";
 localizedStrings["%d More\u2026"] = "%d More\u2026";
+localizedStrings["%d Pass"] = "%d Pass";
 localizedStrings["%d Threads"] = "%d Threads";
+localizedStrings["%d Unsupported"] = "%d Unsupported";
+localizedStrings["%d Warn"] = "%d Warn";
 localizedStrings["%d Warnings"] = "%d Warnings";
 localizedStrings["%d \xd7 %d pixels"] = "%d \xd7 %d pixels";
 localizedStrings["%d \xd7 %d pixels (Natural: %d \xd7 %d pixels)"] = "%d \xd7 %d pixels (Natural: %d \xd7 %d pixels)";
@@ -43,6 +48,7 @@ localizedStrings["%s (hidden)"] = "%s (hidden)";
 localizedStrings["%s Event Dispatched"] = "%s Event Dispatched";
 localizedStrings["%s Fired"] = "%s Fired";
 localizedStrings["%s Prototype"] = "%s Prototype";
+localizedStrings["%s Result"] = "%s Result";
 localizedStrings["%s \u2013 %s"] = "%s \u2013 %s";
 localizedStrings["%s \u2014 %s"] = "%s \u2014 %s";
 localizedStrings["%s cannot be modified"] = "%s cannot be modified";
@@ -88,6 +94,9 @@ localizedStrings["All Layers"] = "All Layers";
 localizedStrings["All Requests"] = "All Requests";
 localizedStrings["All Resources"] = "All Resources";
 localizedStrings["All Storage"] = "All Storage";
+localizedStrings["All items in “%s“ must be error objects"] = "All items in “%s“ must be error objects";
+localizedStrings["All items in “%s“ must be non-empty strings"] = "All items in “%s“ must be non-empty strings";
+localizedStrings["All items in “%s“ must be valid DOM nodes"] = "All items in “%s“ must be valid DOM nodes";
 localizedStrings["An error occurred trying to load the resource."] = "An error occurred trying to load the resource.";
 localizedStrings["An error occurred trying to read the “%s” table."] = "An error occurred trying to read the “%s” table.";
 localizedStrings["An unexpected error %s occurred."] = "An unexpected error %s occurred.";
@@ -113,6 +122,10 @@ localizedStrings["Assertive"] = "Assertive";
 localizedStrings["Attribute"] = "Attribute";
 localizedStrings["Attribute Modified"] = "Attribute Modified";
 localizedStrings["Attributes"] = "Attributes";
+localizedStrings["Audit"] = "Audit";
+localizedStrings["Audit test error: %s"] = "Audit test error: %s";
+localizedStrings["Audit:"] = "Audit:";
+localizedStrings["Audits"] = "Audits";
 localizedStrings["Author Stylesheet"] = "Author Stylesheet";
 localizedStrings["Auto Increment"] = "Auto Increment";
 localizedStrings["Automatically continue after evaluating"] = "Automatically continue after evaluating";
@@ -244,6 +257,7 @@ localizedStrings["DNS"] = "DNS";
 localizedStrings["DOM Content Loaded \u2014 %s"] = "DOM Content Loaded \u2014 %s";
 localizedStrings["DOM Event"] = "DOM Event";
 localizedStrings["DOM Events"] = "DOM Events";
+localizedStrings["DOM Nodes:"] = "DOM Nodes:";
 localizedStrings["Damping"] = "Damping";
 localizedStrings["Data"] = "Data";
 localizedStrings["Data returned from the database is too large."] = "Data returned from the database is too large.";
@@ -329,6 +343,7 @@ localizedStrings["Element is the root element"] = "Element is the root element";
 localizedStrings["Element may overlap another compositing element"] = "Element may overlap another compositing element";
 localizedStrings["Element overlaps other compositing element"] = "Element overlaps other compositing element";
 localizedStrings["Elements"] = "Elements";
+localizedStrings["Enable Audit Tab"] = "Enable Audit Tab";
 localizedStrings["Enable Breakpoint"] = "Enable Breakpoint";
 localizedStrings["Enable Breakpoints"] = "Enable Breakpoints";
 localizedStrings["Enable Event Listener"] = "Enable Event Listener";
@@ -346,6 +361,7 @@ localizedStrings["Entire Recording"] = "Entire Recording";
 localizedStrings["Error"] = "Error";
 localizedStrings["Error: "] = "Error: ";
 localizedStrings["Errors"] = "Errors";
+localizedStrings["Errors:"] = "Errors:";
 localizedStrings["Eval Code"] = "Eval Code";
 localizedStrings["Evaluate JavaScript"] = "Evaluate JavaScript";
 localizedStrings["Event"] = "Event";
@@ -362,6 +378,8 @@ localizedStrings["Experimental"] = "Experimental";
 localizedStrings["Expires"] = "Expires";
 localizedStrings["Export"] = "Export";
 localizedStrings["Export HAR"] = "Export HAR";
+localizedStrings["Export Result"] = "Export Result";
+localizedStrings["Export Test"] = "Export Test";
 localizedStrings["Export recording (%s)"] = "Export recording (%s)";
 localizedStrings["Expression"] = "Expression";
 localizedStrings["Extension Scripts"] = "Extension Scripts";
@@ -453,6 +471,7 @@ localizedStrings["Images"] = "Images";
 localizedStrings["Immediate Pause Requested"] = "Immediate Pause Requested";
 localizedStrings["Import"] = "Import";
 localizedStrings["Import recording from file"] = "Import recording from file";
+localizedStrings["Imported"] = "Imported";
 localizedStrings["Imported Recordings"] = "Imported Recordings";
 localizedStrings["Incomplete"] = "Incomplete";
 localizedStrings["Indent width:"] = "Indent width:";
@@ -537,6 +556,7 @@ localizedStrings["Memory: %s"] = "Memory: %s";
 localizedStrings["Message"] = "Message";
 localizedStrings["Method"] = "Method";
 localizedStrings["Microtask Dispatched"] = "Microtask Dispatched";
+localizedStrings["Missing result level"] = "Missing result level";
 localizedStrings["Mixed"] = "Mixed";
 localizedStrings["Module Code"] = "Module Code";
 localizedStrings["Multi-Entry"] = "Multi-Entry";
@@ -565,9 +585,11 @@ localizedStrings["No Properties"] = "No Properties";
 localizedStrings["No Query Parameters"] = "No Query Parameters";
 localizedStrings["No Request Headers"] = "No Request Headers";
 localizedStrings["No Response Headers"] = "No Response Headers";
+localizedStrings["No Result"] = "No Result";
 localizedStrings["No Results Found"] = "No Results Found";
 localizedStrings["No Search Results"] = "No Search Results";
 localizedStrings["No Watch Expressions"] = "No Watch Expressions";
+localizedStrings["No audit selected"] = "No audit selected";
 localizedStrings["No matching ARIA role"] = "No matching ARIA role";
 localizedStrings["No preview available"] = "No preview available";
 localizedStrings["No request cookies."] = "No request cookies.";
@@ -626,7 +648,9 @@ localizedStrings["Pong Frame"] = "Pong Frame";
 localizedStrings["Port"] = "Port";
 localizedStrings["Prefer indent using:"] = "Prefer indent using:";
 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["Pressed"] = "Pressed";
 localizedStrings["Pretty print"] = "Pretty print";
 localizedStrings["Preview"] = "Preview";
@@ -699,11 +723,14 @@ localizedStrings["Response Headers"] = "Response Headers";
 localizedStrings["Response:"] = "Response:";
 localizedStrings["Restart (%s)"] = "Restart (%s)";
 localizedStrings["Restart animation"] = "Restart animation";
+localizedStrings["Results"] = "Results";
 localizedStrings["Resume Processing"] = "Resume Processing";
 localizedStrings["Resume Thread"] = "Resume Thread";
 localizedStrings["Retained Size"] = "Retained Size";
+localizedStrings["Return string must be one of %s"] = "Return string must be one of %s";
 localizedStrings["Return type for anonymous function"] = "Return type for anonymous function";
 localizedStrings["Return type for function: %s"] = "Return type for function: %s";
+localizedStrings["Return value is not an object, string, or boolean"] = "Return value is not an object, string, or boolean";
 localizedStrings["Reveal Breakpoint"] = "Reveal Breakpoint";
 localizedStrings["Reveal in DOM Tree"] = "Reveal in DOM Tree";
 localizedStrings["Reveal in Debugger Tab"] = "Reveal in Debugger Tab";
@@ -713,6 +740,8 @@ localizedStrings["Reveal in Network Tab"] = "Reveal in Network Tab";
 localizedStrings["Reveal in Original Resource"] = "Reveal in Original Resource";
 localizedStrings["Reveal in Resources Tab"] = "Reveal in Resources Tab";
 localizedStrings["Role"] = "Role";
+localizedStrings["Run %d"] = "Run %d";
+localizedStrings["Running the “%s“ audit"] = "Running the “%s“ audit";
 localizedStrings["Same-Site"] = "Same-Site";
 localizedStrings["Samples"] = "Samples";
 localizedStrings["Save File"] = "Save File";
@@ -790,6 +819,7 @@ localizedStrings["Show transparency grid"] = "Show transparency grid";
 localizedStrings["Show type information"] = "Show type information";
 localizedStrings["Show warnings logged to the Console"] = "Show warnings logged to the Console";
 localizedStrings["Show:"] = "Show:";
+localizedStrings["Showing:"] = "Showing:";
 localizedStrings["Size"] = "Size";
 localizedStrings["Size of current object plus all objects it keeps alive"] = "Size of current object plus all objects it keeps alive";
 localizedStrings["Sizes"] = "Sizes";
@@ -809,6 +839,7 @@ localizedStrings["Specificity: (%d, %d, %d)"] = "Specificity: (%d, %d, %d)";
 localizedStrings["Specificity: No value for selected element"] = "Specificity: No value for selected element";
 localizedStrings["Spelling"] = "Spelling";
 localizedStrings["Stalled"] = "Stalled";
+localizedStrings["Start"] = "Start";
 localizedStrings["Start Time"] = "Start Time";
 localizedStrings["Start element selection (%s)"] = "Start element selection (%s)";
 localizedStrings["Start recording (%s)\nCreate new recording (%s)"] = "Start recording (%s)\nCreate new recording (%s)";
@@ -821,11 +852,13 @@ localizedStrings["Step into (%s or %s)"] = "Step into (%s or %s)";
 localizedStrings["Step out (%s or %s)"] = "Step out (%s or %s)";
 localizedStrings["Step over (%s or %s)"] = "Step over (%s or %s)";
 localizedStrings["Stiffness"] = "Stiffness";
+localizedStrings["Stop"] = "Stop";
 localizedStrings["Stop Recording"] = "Stop Recording";
 localizedStrings["Stop element selection (%s)"] = "Stop element selection (%s)";
 localizedStrings["Stop recording"] = "Stop recording";
 localizedStrings["Stop recording (%s)"] = "Stop recording (%s)";
 localizedStrings["Stop recording canvas actions"] = "Stop recording canvas actions";
+localizedStrings["Stopping the “%s“ audit"] = "Stopping the “%s“ audit";
 localizedStrings["Storage"] = "Storage";
 localizedStrings["Style Attribute"] = "Style Attribute";
 localizedStrings["Style rule"] = "Style rule";
@@ -849,6 +882,11 @@ localizedStrings["Text"] = "Text";
 localizedStrings["Text Frame"] = "Text Frame";
 localizedStrings["Text Node"] = "Text Node";
 localizedStrings["The page's content has changed"] = "The page's content has changed";
+localizedStrings["The “%s“ audit failed"] = "The “%s“ audit failed";
+localizedStrings["The “%s“ audit is unsupported"] = "The “%s“ audit is unsupported";
+localizedStrings["The “%s“ audit passed"] = "The “%s“ audit passed";
+localizedStrings["The “%s“ audit threw an error"] = "The “%s“ audit threw an error";
+localizedStrings["The “%s“ audit warned"] = "The “%s“ audit warned";
 localizedStrings["The “%s”\ntable is empty."] = "The “%s”\ntable is empty.";
 localizedStrings["This action causes no visual change"] = "This action causes no visual change";
 localizedStrings["This action moves the path outside the visible area"] = "This action moves the path outside the visible area";
@@ -939,6 +977,7 @@ localizedStrings["Zoom:"] = "Zoom:";
 localizedStrings["computed"] = "computed";
 localizedStrings["default"] = "default";
 localizedStrings["for changes to take effect"] = "for changes to take effect";
+localizedStrings["invalid JSON."] = "invalid JSON.";
 localizedStrings["key"] = "key";
 localizedStrings["line "] = "line ";
 localizedStrings["originally %s"] = "originally %s";
@@ -951,6 +990,8 @@ localizedStrings["toggle"] = "toggle";
 localizedStrings["unsupported version."] = "unsupported version.";
 localizedStrings["value"] = "value";
 localizedStrings["“%s“ Event Fired"] = "“%s“ Event Fired";
+localizedStrings["“%s“ must be a %s"] = "“%s“ must be a %s";
+localizedStrings["“%s“ must be an %s"] = "“%s“ must be an %s";
 localizedStrings["“%s” Profile Recorded"] = "“%s” Profile Recorded";
 localizedStrings["“%s” is invalid."] = "“%s” is invalid.";
 localizedStrings["“%s” threw an error."] = "“%s” threw an error.";
index 3190023..fcb0e4f 100644 (file)
@@ -127,6 +127,7 @@ WI.loaded = function()
     this.workerManager = new WI.WorkerManager;
     this.domDebuggerManager = new WI.DOMDebuggerManager;
     this.canvasManager = new WI.CanvasManager;
+    this.auditManager = new WI.AuditManager;
 
     // Enable the Console Agent after creating the singleton managers.
     ConsoleAgent.enable();
@@ -446,6 +447,7 @@ WI.contentLoaded = function()
         WI.StorageTabContentView,
         WI.CanvasTabContentView,
         WI.LayersTabContentView,
+        WI.AuditTabContentView,
         WI.ConsoleTabContentView,
         WI.SearchTabContentView,
         WI.NewTabContentView,
@@ -1114,6 +1116,10 @@ WI.tabContentViewClassForRepresentedObject = function(representedObject)
         representedObject instanceof WI.IndexedDatabase || representedObject instanceof WI.IndexedDatabaseObjectStoreIndex)
         return WI.StorageTabContentView;
 
+    if (representedObject instanceof WI.AuditTestCase || representedObject instanceof WI.AuditTestGroup
+        || representedObject instanceof WI.AuditTestCaseResult || representedObject instanceof WI.AuditTestGroupResult)
+        return WI.AuditTabContentView;
+
     if (representedObject instanceof WI.CanvasCollection)
         return WI.CanvasTabContentView;
 
index ece64e0..cc2788d 100644 (file)
@@ -125,6 +125,7 @@ WI.settings = {
     experimentalEnableMultiplePropertiesSelection: new WI.Setting("experimental-enable-multiple-properties-selection", false),
     experimentalEnableLayersTab: new WI.Setting("experimental-enable-layers-tab", false),
     experimentalEnableNewTabBar: new WI.Setting("experimental-enable-new-tab-bar", false),
+    experimentalEnableAuditTab: new WI.Setting("experimental-enable-audit-tab", false),
 
     // DebugUI
     autoLogProtocolMessages: new WI.Setting("auto-collect-protocol-messages", false),
index 69035cc..0a22daf 100644 (file)
@@ -1372,6 +1372,17 @@ Object.defineProperty(Array.prototype, "binaryIndexOf",
     }
 });
 
+Object.defineProperty(Promise, "chain",
+{
+    async value(callbacks, initialValue)
+    {
+        let results = [];
+        for (let i = 0; i < callbacks.length; ++i)
+            results.push(await callbacks[i](results.lastValue || initialValue || null, i));
+        return results;
+    }
+});
+
 Object.defineProperty(Promise, "delay",
 {
     value(delay)
index 5bdc42a..e0defa0 100644 (file)
@@ -29,111 +29,148 @@ WI.AuditManager = class AuditManager extends WI.Object
     {
         super();
 
-        this._testSuiteConstructors = [];
-        this._reports = new Map;
+        this._tests = [];
+        this._results = [];
 
-        // Transforming all the constructors into AuditTestSuite instances.
-        this._testSuites = this._testSuiteConstructors.map(suite => {
-            let newTestSuite = new suite;
+        this._runningState = WI.AuditManager.RunningState.Inactive;
+        this._runningTests = [];
+    }
 
-            if (!(newTestSuite instanceof WI.AuditTestSuite))
-                throw new Error("Audit test suites must be of instance WI.AuditTestSuite.");
+    static synthesizeError(message)
+    {
+        let consoleMessage = new WI.ConsoleMessage(WI.mainTarget, WI.ConsoleMessage.MessageSource.Other, WI.ConsoleMessage.MessageLevel.Error, WI.UIString("Audit test error: %s").format(message));
+        consoleMessage.shouldRevealConsole = true;
 
-            return newTestSuite;
-        });
+        WI.consoleLogViewController.appendConsoleMessage(consoleMessage);
     }
 
     // Public
 
-    get testSuites() { return this._testSuites.slice(); }
-    get reports() { return [...this._reports.values()]; }
+    get tests() { return this._tests; }
+    get results() { return this._results; }
+    get runningState() { return this._runningState; }
 
-    async runAuditTestByRepresentedObject(representedObject)
+    async start(tests)
     {
-        let auditReport = new WI.AuditReport(representedObject);
-
-        if (representedObject instanceof WI.AuditTestCase) {
-            let auditResult = await this._runTestCase(representedObject);
-            auditReport.addResult(auditResult);
-        } else if (representedObject instanceof WI.AuditTestSuite) {
-            let testCases = representedObject.testCases;
-            // Start reducing from testCases[0].
-            let result = testCases.slice(1).reduce((chain, testCase, index) => {
-                if (testCase.setup) {
-                    let setup = testCase.setup.call(testCase, testCase.suite);
-                    if (testCase.setup[Symbol.toStringTag] === "AsyncFunction")
-                        return setup;
-                    else
-                        return new Promise(setup);
-                }
+        console.assert(this._runningState === WI.AuditManager.RunningState.Inactive);
+        if (this._runningState !== WI.AuditManager.RunningState.Inactive)
+            return;
 
-                chain = chain.then((auditResult) => {
-                    auditReport.addResult(auditResult);
-                    return this._runTestCase(testCase);
-                });
-
-                if (testCase.tearDown) {
-                    let tearDown = testCase.tearDown.call(testCase, testCase.suite);
-                    if (testCase.tearDown[Symbol.toStringTag] === "AsyncFunction")
-                        return tearDown;
-                    else
-                        return new Promise(tearDown);
-                }
-                return chain;
-            }, this._runTestCase(testCases[0]));
+        if (tests && tests.length)
+            tests = tests.filter((test) => typeof test === "object" && test instanceof WI.AuditTestBase);
+        else
+            tests = this._tests;
+
+        if (!tests.length)
+            return;
+
+        this._runningState = WI.AuditManager.RunningState.Active;
+        this._runningTests = tests;
+        for (let test of this._runningTests)
+            test.clearResult();
 
-            let lastAuditResult = await result;
-            auditReport.addResult(lastAuditResult);
+        this.dispatchEventToListeners(WI.AuditManager.Event.TestScheduled);
 
-            // Make AuditReport read-only after all the AuditResults have been received.
-            auditReport.close();
-        }
+        await Promise.chain(this._runningTests.map((test) => () => this._runningState === WI.AuditManager.RunningState.Active ? test.start() : null));
 
-        this._reports.set(representedObject.id, auditReport);
-        this.dispatchEventToListeners(WI.AuditManager.Event.NewReportAdded, {auditReport});
+        let result = this._runningTests.map((test) => test.result).filter((result) => !!result);
 
-        return auditReport;
+        this._runningState = WI.AuditManager.RunningState.Inactive;
+        this._runningTests = [];
+
+        this._addResult(result);
     }
 
-    addTestSuite(auditTestSuiteConstructor)
+    stop()
     {
-        if (this._testSuiteConstructors.indexOf(auditTestSuiteConstructor) >= 0)
-            throw new Error(`class ${auditTestSuiteConstructor.name} already exists.`);
+        console.assert(this._runningState === WI.AuditManager.RunningState.Active);
+        if (this._runningState !== WI.AuditManager.RunningState.Active)
+            return;
+
+        for (let test of this._runningTests)
+            test.stop();
 
-        let auditTestSuite = new auditTestSuiteConstructor;
-        this._testSuiteConstructors.push(auditTestSuiteConstructor);
-        this._testSuites.push(auditTestSuite);
+        this._runningState = WI.AuditManager.RunningState.Stopping;
     }
 
-    reportForId(reportId)
+    import()
     {
-        return this._reports.get(reportId);
+        WI.loadDataFromFile((data, filename) => {
+            if (!data)
+                return;
+
+            let payload = null;
+            try {
+                payload = JSON.parse(data);
+            } catch (e) {
+                WI.AuditManager.synthesizeError(e);
+                return;
+            }
+
+            let object = WI.AuditTestGroup.fromPayload(payload) || WI.AuditTestCase.fromPayload(payload);
+            if (!object) {
+                object = WI.AuditTestGroupResult.fromPayload(payload) || WI.AuditTestCaseResult.fromPayload(payload);
+                if (!object) {
+                    WI.AuditManager.synthesizeError(WI.UIString("invalid JSON."));
+                    return;
+                }
+            }
+
+            if (object instanceof WI.AuditTestBase)
+                this._addTest(object);
+            else if (object instanceof WI.AuditTestResultBase)
+                this._addResult(object);
+        });
     }
 
-    removeAllReports()
+    export(object)
     {
-        this._reports.clear();
+        console.assert(object instanceof WI.AuditTestCase || object instanceof WI.AuditTestGroup || object instanceof WI.AuditTestCaseResult || object instanceof WI.AuditTestGroupResult, object);
+
+        let filename = object.name;
+        if (object instanceof WI.AuditTestResultBase)
+            filename = WI.UIString("%s Result").format(filename);
+
+        let url = "web-inspector:///" + encodeURI(filename) + ".json";
+
+        WI.saveDataToFile({
+            url,
+            content: JSON.stringify(object),
+            forceSaveAs: true,
+        });
     }
 
     // Private
 
-    async _runTestCase(testCase)
+    _addTest(test)
+    {
+        this._tests.push(test);
+
+        this.dispatchEventToListeners(WI.AuditManager.Event.TestAdded, {test});
+    }
+
+    _addResult(result)
     {
-        let didRaiseException = false;
-        let result;
-        this.dispatchEventToListeners(WI.AuditManager.Event.TestStarted, {test: testCase});
-        try {
-            result = await testCase.test.call(testCase, testCase.suite);
-        } catch (resultData) {
-            result = resultData;
-            didRaiseException = true;
-        }
-        this.dispatchEventToListeners(WI.AuditManager.Event.TestEnded, {test: testCase});
-        return new WI.AuditResult(testCase, {result}, didRaiseException);
+        if (!result || (Array.isArray(result) && !result.length))
+            return;
+
+        this._results.push(result);
+
+        this.dispatchEventToListeners(WI.AuditManager.Event.TestCompleted, {
+            result,
+            index: this._results.length - 1,
+        });
     }
 };
 
+WI.AuditManager.RunningState = {
+    Inactive: "inactive",
+    Active: "active",
+    Stopping: "stopping",
+};
+
 WI.AuditManager.Event = {
-    TestStarted: Symbol("test-started"),
-    TestEnded: Symbol("test-ended")
+    TestAdded: "audit-manager-test-added",
+    TestCompleted: "audit-manager-test-completed",
+    TestScheduled: "audit-manager-test-scheduled",
 };
diff --git a/Source/WebInspectorUI/UserInterface/Images/Audit.svg b/Source/WebInspectorUI/UserInterface/Images/Audit.svg
new file mode 100644 (file)
index 0000000..694d341
--- /dev/null
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright © 2018 Apple Inc. All rights reserved. -->
+<svg xmlns="http://www.w3.org/2000/svg" id="root" version="1.1" viewBox="0 0 16 16">
+    <rect fill="none" stroke="currentColor" x="0.5" y="0.5" width="15" height="15" rx="2"/>
+    <path fill="none" stroke-linecap="round" stroke="currentColor" d="M 3.5 5.5 L 12.5 5.5"/>
+    <path fill="none" stroke-linecap="round" stroke="currentColor" d="M 12.5 5.5 L 9.5 2.5"/>
+    <path fill="none" stroke-linecap="round" stroke="currentColor" d="M 12.5 5.5 L 9.5 8.5"/>
+    <path fill="none" stroke-linecap="round" stroke="currentColor" d="M 3.5 10.5 L 12.5 10.5"/>
+    <path fill="none" stroke-linecap="round" stroke="currentColor" d="M 3.5 10.5 L 6.5 7.5"/>
+    <path fill="none" stroke-linecap="round" stroke="currentColor" d="M 3.5 10.5 L 6.5 13.5"/>
+</svg>
diff --git a/Source/WebInspectorUI/UserInterface/Images/AuditStart.svg b/Source/WebInspectorUI/UserInterface/Images/AuditStart.svg
new file mode 100644 (file)
index 0000000..6b9f323
--- /dev/null
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright © 2018 Apple Inc. All rights reserved. -->
+<svg xmlns="http://www.w3.org/2000/svg" id="root" version="1.1" viewBox="0 0 12 12">
+    <path fill="currentColor" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="M 11 6 L 2 1 v 10 z"/>
+</svg>
diff --git a/Source/WebInspectorUI/UserInterface/Images/AuditStop.svg b/Source/WebInspectorUI/UserInterface/Images/AuditStop.svg
new file mode 100644 (file)
index 0000000..9dd0600
--- /dev/null
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright © 2018 Apple Inc. All rights reserved. -->
+<svg xmlns="http://www.w3.org/2000/svg" id="root" version="1.1" viewBox="0 0 12 12">
+    <path fill="currentColor" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="M 2 2 h 8 v 8 H 2 z"/>
+</svg>
diff --git a/Source/WebInspectorUI/UserInterface/Images/AuditTestCase.svg b/Source/WebInspectorUI/UserInterface/Images/AuditTestCase.svg
new file mode 100644 (file)
index 0000000..b4578fc
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright © 2018 Apple Inc. All rights reserved. -->
+<svg xmlns="http://www.w3.org/2000/svg" id="root" version="1.1" viewBox="0 0 16 16">
+    <path fill="rgb(148, 183, 219)" d="M 13 1 L 3 1 C 1.898438 1 1 1.898438 1 3 L 1 13 C 1 14.101562 1.898438 15 3 15 L 13 15 C 14.101562 15 15 14.101562 15 13 L 15 3 C 15 1.898438 14.101562 1 13 1 Z"/>
+    <path fill="rgb(106, 136, 170)" d="M 13 1 L 3 1 C 1.898438 1 1 1.898438 1 3 L 1 13 C 1 14.101562 1.898438 15 3 15 L 13 15 C 14.101562 15 15 14.101562 15 13 L 15 3 C 15 1.898438 14.101562 1 13 1 M 13 2 C 13.550781 2 14 2.449219 14 3 L 14 13 C 14 13.550781 13.550781 14 13 14 L 3 14 C 2.449219 14 2 13.550781 2 13 L 2 3 C 2 2.449219 2.449219 2 3 2 L 13 2"/>
+    <path fill="white" d="M 9.1 12.4 c -1.8 0 -2.6 -0.7 -2.6 -2.3 V 7.4 H 4.6 V 5.1 h 1.9 V 3.6 h 2.6 v 1.5 h 2.2 v 2.3 H 9.2 v 2.7 h 0.6 l 0.6 -0.1 v 2.2 l -0.4 0.1 c -0.2 0 -0.5 0.1 -0.9 0.1 z"/>
+    <path fill="rgb(113, 146, 184)" d="M 8.7 4.1 v 1.5 h 2.2 v 1.3 H 8.7 v 3 c 0 0.6 0.3 0.8 0.9 0.8 h 0.3 v 1.2 c -0.1 0 -0.4 0.1 -0.7 0.1 -1.6 0 -2.1 -0.5 -2.1 -1.8 V 6.9 h -2 V 5.6 H 7 V 4.1 h 1.7 m 1 -1 H 6.1 v 1.5 h -2 v 3.3 H 6 v 2.2 c 0 1.9 1.1 2.8 3.1 2.8 0.3 0 0.7 0 0.9 -0.1 l 0.8 -0.2 V 9.4 l -1.1 0.2 h -0.1 V 7.9 h 2.2 V 4.6 H 9.7 V 3.1 sz"/>
+</svg>
diff --git a/Source/WebInspectorUI/UserInterface/Images/AuditTestCaseResult.svg b/Source/WebInspectorUI/UserInterface/Images/AuditTestCaseResult.svg
new file mode 100644 (file)
index 0000000..f7d63a2
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright © 2018 Apple Inc. All rights reserved. -->
+<svg xmlns="http://www.w3.org/2000/svg" id="root" version="1.1" viewBox="0 0 16 16">
+    <path fill="rgb(203, 203, 203)" d="M 13 1 L 3 1 C 1.898438 1 1 1.898438 1 3 L 1 13 C 1 14.101562 1.898438 15 3 15 L 13 15 C 14.101562 15 15 14.101562 15 13 L 15 3 C 15 1.898438 14.101562 1 13 1 Z"/>
+    <path fill="rgb(153, 153, 153)" d="M 13 1 L 3 1 C 1.898438 1 1 1.898438 1 3 L 1 13 C 1 14.101562 1.898438 15 3 15 L 13 15 C 14.101562 15 15 14.101562 15 13 L 15 3 C 15 1.898438 14.101562 1 13 1 M 13 2 C 13.550781 2 14 2.449219 14 3 L 14 13 C 14 13.550781 13.550781 14 13 14 L 3 14 C 2.449219 14 2 13.550781 2 13 L 2 3 C 2 2.449219 2.449219 2 3 2 L 13 2"/>
+    <path fill="white" d="M 9.1 12.4 c -1.8 0 -2.6 -0.7 -2.6 -2.3 V 7.4 H 4.6 V 5.1 h 1.9 V 3.6 h 2.6 v 1.5 h 2.2 v 2.3 H 9.2 v 2.7 h 0.6 l 0.6 -0.1 v 2.2 l -0.4 0.1 c -0.2 0 -0.5 0.1 -0.9 0.1 z"/>
+    <path fill="rgb(166, 166, 166)" d="M 8.7 4.1 v 1.5 h 2.2 v 1.3 H 8.7 v 3 c 0 0.6 0.3 0.8 0.9 0.8 h 0.3 v 1.2 c -0.1 0 -0.4 0.1 -0.7 0.1 -1.6 0 -2.1 -0.5 -2.1 -1.8 V 6.9 h -2 V 5.6 H 7 V 4.1 h 1.7 m 1 -1 H 6.1 v 1.5 h -2 v 3.3 H 6 v 2.2 c 0 1.9 1.1 2.8 3.1 2.8 0.3 0 0.7 0 0.9 -0.1 l 0.8 -0.2 V 9.4 l -1.1 0.2 h -0.1 V 7.9 h 2.2 V 4.6 H 9.7 V 3.1 sz"/>
+</svg>
diff --git a/Source/WebInspectorUI/UserInterface/Images/AuditTestError.svg b/Source/WebInspectorUI/UserInterface/Images/AuditTestError.svg
new file mode 100644 (file)
index 0000000..6fade3c
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright © 2018 Apple Inc. All rights reserved. -->
+<svg xmlns="http://www.w3.org/2000/svg" id="root" version="1.1" viewBox="0 0 16 16">
+    <path fill="rgb(216, 82, 92)" stroke="rgb(219, 25, 34)" stroke-width="0.5" stroke-miterlimit="10" d="M 10.7 1.5 H 5.3 L 1.5 5.3 v 5.4 l 3.8 3.8 h 5.4 l 3.8 -3.8 V 5.3 z"/>
+    <path fill="white" d="M 8 9.165 c -0.588 0 -1.165 -0.665 -1.165 -1.341 V 4.176 c 0 -0.802 0.468 -1.341 1.165 -1.341 s 1.165 0.539 1.165 1.341 v 3.648 c 0 0.677 -0.577 1.341 -1.165 1.341 z"/>
+    <path fill="rgb(219, 25, 34)" d="M 8 3 c 0.6 0 1 0.47 1 1.177 v 3.647 C 9 8.412 8.5 9 8 9 s -1 -0.588 -1 -1.176 V 4.177 C 7 3.47 7.4 3 8 3 m 0 -0.33 c -0.783 0 -1.33.62 -1.33 1.506 v 3.648 c 0 0.774 0.646 1.506 1.33 1.506 s 1.33 -0.732 1.33 -1.506 V 4.177 C 9.33 3.29 8.783 2.67 8 2.67 z"/>
+    <circle fill="white" cx="8" cy="11.5" r="1.415"/>
+    <path fill="rgb(219, 25, 34)" d="M 8 10.25 a 1.25 1.25 0 1 1 0 2.5 1.25 1.25 0 0 1 0 -2.5 m 0 -0.33 c -0.871 0 -1.58 0.709 -1.58 1.58 s 0.709 1.58 1.58 1.58 1.58 -0.709 1.58 -1.58 S 8.871 9.92 8 9.92 z"/>
+</svg>
diff --git a/Source/WebInspectorUI/UserInterface/Images/AuditTestFail.svg b/Source/WebInspectorUI/UserInterface/Images/AuditTestFail.svg
new file mode 100644 (file)
index 0000000..14e8bb3
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright © 2018 Apple Inc. All rights reserved. -->
+<svg xmlns="http://www.w3.org/2000/svg" id="root" version="1.1" viewBox="0 0 16 16">
+    <path fill="rgb(216, 82, 92)" stroke="rgb(219, 25, 34)" stroke-width="0.5" stroke-miterlimit="10" d="M 7.3 14.7 l -6 -6 c -0.4 -0.4 -0.4 -1.1 0 -1.5 l 6 -6 c 0.4 -0.4 1.1 -0.4 1.5 0 l 6 6 c 0.4 0.4 0.4 1.1 0 1.5 l -6 6 c -0.5 0.4 -1.1 0.4 -1.5 0 z"/>
+    <path fill="white" d="M 10.191 11.206 a 0.162 0.162 0 0 1 -0.116 -0.049 L 8 9.082 l -2.075 2.075 a 0.164 0.164 0 0 1 -0.234 0 l -0.848 -0.848 a 0.164 0.164 0 0 1 0 -0.233 L 6.918 8 4.843 5.925 a 0.165 0.165 0 0 1 0 -0.234 l 0.848 -0.848 a 0.165 0.165 0 0 1 0.234 0 L 8 6.918 l 2.075 -2.075 a 0.163 0.163 0 0 1 0.233 0 l 0.848 0.848 a 0.165 0.165 0 0 1 0 0.234 L 9.082 8 l 2.075 2.076 a 0.165 0.165 0 0 1 0 0.233l -0.85 0.848 a 0.161 0.161 0 0 1 -0.116 0.05 z"/>
+    <path fill="rgb(219, 25, 34)" d="M 10.192 4.96 l 0.848 0.848 L 8.848 8 l 2.192 2.192 -0.848 0.849 L 8 8.848 5.808 11.04 l -0.849 -0.848 L 7.151 8 4.96 5.808 l 0.849 -0.849 L 8 7.152 l 2.192 -2.192 m 0 -0.33 a 0.329 0.329 0 0 0 -0.233 0.096 l -1.96 1.959 -1.958 -1.959 a 0.329 0.329 0 0 0 -0.467 0 l -0.848 0.849 a 0.33 0.33 0 0 0 0 0.466 L 6.685 8 4.726 9.959 a 0.33 0.33 0 0 0 0 0.466 l 0.849 0.849 a 0.33 0.33 0 0 0 0.466 0 L 8 9.315 l 1.959 1.959 a 0.33 0.33 0 0 0 0.466 0 l 0.849 -0.849 a 0.33 0.33 0 0 0 0 -0.466 L 9.315 8 l 1.959 -1.959 a 0.33 0.33 0 0 0 0 -0.466 l -0.849 -0.849 a 0.33 0.33 0 0 0 -0.233 -0.096 z"/>
+</svg>
diff --git a/Source/WebInspectorUI/UserInterface/Images/AuditTestGroup.svg b/Source/WebInspectorUI/UserInterface/Images/AuditTestGroup.svg
new file mode 100644 (file)
index 0000000..f94ac63
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright © 2018 Apple Inc. All rights reserved. -->
+<svg xmlns="http://www.w3.org/2000/svg" id="root" version="1.1" viewBox="0 0 16 16">
+    <path fill="rgb(148, 183, 219)" d="M 13 1 L 3 1 C 1.898438 1 1 1.898438 1 3 L 1 13 C 1 14.101562 1.898438 15 3 15 L 13 15 C 14.101562 15 15 14.101562 15 13 L 15 3 C 15 1.898438 14.101562 1 13 1 Z"/>
+    <path fill="rgb(106, 136, 170)" d="M 13 1 L 3 1 C 1.898438 1 1 1.898438 1 3 L 1 13 C 1 14.101562 1.898438 15 3 15 L 13 15 C 14.101562 15 15 14.101562 15 13 L 15 3 C 15 1.898438 14.101562 1 13 1 M 13 2 C 13.550781 2 14 2.449219 14 3 L 14 13 C 14 13.550781 13.550781 14 13 14 L 3 14 C 2.449219 14 2 13.550781 2 13 L 2 3 C 2 2.449219 2.449219 2 3 2 L 13 2"/>
+    <path fill="rgb(113, 146, 184)" d="M 6.980469 12.742188 C 6.429688 12.742188 5.980469 12.292969 5.980469 11.742188 C 5.980469 11.742188 5.980469 7.34375 5.980469 5.882812 C 5.140625 5.882812 4.136719 5.882812 4.136719 5.882812 C 3.582031 5.882812 3.136719 5.4375 3.136719 4.882812 L 3.136719 3.792969 C 3.136719 3.242188 3.582031 2.792969 4.136719 2.792969 L 11.488281 2.792969 C 12.042969 2.792969 12.488281 3.242188 12.488281 3.792969 L 12.488281 4.882812 C 12.488281 5.4375 12.042969 5.882812 11.488281 5.882812 C 11.488281 5.882812 10.484375 5.882812 9.640625 5.882812 C 9.640625 7.34375 9.640625 11.742188 9.640625 11.742188 C 9.640625 12.292969 9.195312 12.742188 8.640625 12.742188 L 6.980469 12.742188"/>
+    <path fill="white" d="M 6.980469 11.742188 L 6.980469 4.882812 L 4.136719 4.882812 L 4.136719 3.792969 L 11.488281 3.792969 L 11.488281 4.882812 L 8.640625 4.882812 L 8.640625 11.742188 Z"/>
+</svg>
diff --git a/Source/WebInspectorUI/UserInterface/Images/AuditTestGroupResult.svg b/Source/WebInspectorUI/UserInterface/Images/AuditTestGroupResult.svg
new file mode 100644 (file)
index 0000000..8e3b3ea
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright © 2018 Apple Inc. All rights reserved. -->
+<svg xmlns="http://www.w3.org/2000/svg" id="root" version="1.1" viewBox="0 0 16 16">
+    <path fill="rgb(203, 203, 203)" d="M 13 1 L 3 1 C 1.898438 1 1 1.898438 1 3 L 1 13 C 1 14.101562 1.898438 15 3 15 L 13 15 C 14.101562 15 15 14.101562 15 13 L 15 3 C 15 1.898438 14.101562 1 13 1 Z"/>
+    <path fill="rgb(153, 153, 153)" d="M 13 1 L 3 1 C 1.898438 1 1 1.898438 1 3 L 1 13 C 1 14.101562 1.898438 15 3 15 L 13 15 C 14.101562 15 15 14.101562 15 13 L 15 3 C 15 1.898438 14.101562 1 13 1 M 13 2 C 13.550781 2 14 2.449219 14 3 L 14 13 C 14 13.550781 13.550781 14 13 14 L 3 14 C 2.449219 14 2 13.550781 2 13 L 2 3 C 2 2.449219 2.449219 2 3 2 L 13 2"/>
+    <path fill="rgb(166, 166, 166)" d="M 6.980469 12.742188 C 6.429688 12.742188 5.980469 12.292969 5.980469 11.742188 C 5.980469 11.742188 5.980469 7.34375 5.980469 5.882812 C 5.140625 5.882812 4.136719 5.882812 4.136719 5.882812 C 3.582031 5.882812 3.136719 5.4375 3.136719 4.882812 L 3.136719 3.792969 C 3.136719 3.242188 3.582031 2.792969 4.136719 2.792969 L 11.488281 2.792969 C 12.042969 2.792969 12.488281 3.242188 12.488281 3.792969 L 12.488281 4.882812 C 12.488281 5.4375 12.042969 5.882812 11.488281 5.882812 C 11.488281 5.882812 10.484375 5.882812 9.640625 5.882812 C 9.640625 7.34375 9.640625 11.742188 9.640625 11.742188 C 9.640625 12.292969 9.195312 12.742188 8.640625 12.742188 L 6.980469 12.742188"/>
+    <path fill="white" d="M 6.980469 11.742188 L 6.980469 4.882812 L 4.136719 4.882812 L 4.136719 3.792969 L 11.488281 3.792969 L 11.488281 4.882812 L 8.640625 4.882812 L 8.640625 11.742188 Z"/>
+</svg>
diff --git a/Source/WebInspectorUI/UserInterface/Images/AuditTestNoResult.svg b/Source/WebInspectorUI/UserInterface/Images/AuditTestNoResult.svg
new file mode 100644 (file)
index 0000000..ae09a6a
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright © 2018 Apple Inc. All rights reserved. -->
+<svg xmlns="http://www.w3.org/2000/svg" id="root" version="1.1" viewBox="0 0 16 16">
+    <circle fill="rgb(202, 202, 202)" stroke="rgb(153, 153, 153)" stroke-width="0.5" stroke-miterlimit="10" cx="8" cy="8" r="6.5"/>
+    <path fill="white" stroke="rgb(153, 153, 153)" stroke-width="0.33" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="M 9.25 11.563 a 1.25 1.25 0 1 1 -2.5 0 1.25 1.25 0 0 1 2.5 0 z m -0.973 -8.25 c 1.653 0 2.973 0.832 2.973 2.486 0 1.159 -0.642 1.622 -1.23 1.992 -0.708 0.444 -1 0.632 -1 1.271 v 0.25 H 6.951 l -0.018 -0.131 c -0.136 -1.015 0.137 -1.722 0.946 -2.205 0.559 -0.332 0.933 -0.563 0.933 -1.052 0 -0.47 -0.339 -0.695 -0.69 -0.695 -0.422 0 -0.683 0.338 -0.695 0.783 h -2.17 c -0.12 -1.854 1.35 -2.7 3.02 -2.7 z"/>
+</svg>
diff --git a/Source/WebInspectorUI/UserInterface/Images/AuditTestPass.svg b/Source/WebInspectorUI/UserInterface/Images/AuditTestPass.svg
new file mode 100644 (file)
index 0000000..e9ffd9b
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright © 2018 Apple Inc. All rights reserved. -->
+<svg xmlns="http://www.w3.org/2000/svg" id="root" version="1.1" viewBox="0 0 16 16">
+    <path fill="rgb(181, 201, 116)" stroke="rgb(148, 172, 85)" stroke-width="0.5" stroke-miterlimit="10" d="M 7.3 14.7 l -6 -6 c -0.4 -0.4 -0.4 -1.1 0 -1.5 l 6 -6 c 0.4 -0.4 1.1 -0.4 1.5 0 l 6 6 c 0.4 0.4 0.4 1.1 0 1.5 l -6 6 c -0.5 0.4 -1.1 0.4 -1.5 0 z"/>
+    <path fill="white" d="M 6.9 11.465 a 0.164 0.164 0 0 1 -0.127 -0.06 l -2.4 -2.9 a 0.165 0.165 0 0 1 -0.002 -0.208 l 0.8 -1 a 0.165 0.165 0 0 1 0.127 -0.062 H 5.3 c 0.049 0 0.095 0.021 0.126 0.058 l 1.472 1.749 3.175 -3.847 a 0.165 0.165 0 0 1 0.127 -0.06 h 0.001 c 0.05 0 0.097 0.023 0.128 0.062 l 0.8 1 a 0.165 0.165 0 0 1 -0.001 0.207 l -4.1 5 a 0.164 0.164 0 0 1 -0.128 0.06 z"/>
+    <path fill="rgb(148, 172, 85)" d="M 10.2 5.3 l 0.8 1 -4.1 5 -2.4 -2.9 0.8 -1 1.6 1.9 3.3 -4 m 0 -0.33 a 0.33 0.33 0 0 0 -0.255 0.12 L 6.897 8.785 5.552 7.187 A 0.33 0.33 0 0 0 5.3 7.07 h -0.004 a 0.33 0.33 0 0 0 -0.254 0.124 l -0.8 1 a 0.33 0.33 0 0 0 0.004 0.416 l 2.4 2.9 a 0.33 0.33 0 0 0 0.51 -0.001 l 4.1 -5 a 0.33 0.33 0 0 0 0.002 -0.415 l -0.8 -1 a 0.33 0.33 0 0 0 -0.256 -0.124 H 10.2 z"/>
+</svg>
diff --git a/Source/WebInspectorUI/UserInterface/Images/AuditTestUnsupported.svg b/Source/WebInspectorUI/UserInterface/Images/AuditTestUnsupported.svg
new file mode 100644 (file)
index 0000000..ecad5e8
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright © 2018 Apple Inc. All rights reserved. -->
+<svg xmlns="http://www.w3.org/2000/svg" id="root" version="1.1" viewBox="0 0 16 16">
+    <path fill="rgb(202, 202, 202)" stroke="rgb(153, 153, 153)" stroke-width="0.5" stroke-miterlimit="10" d="M 7.3 14.7 l -6 -6 c -0.4 -0.4 -0.4 -1.1 0 -1.5 l 6 -6 c 0.4 -0.4 1.1 -0.4 1.5 0 l 6 6 c 0.4 0.4 0.4 1.1 0 1.5 l -6 6 c -0.5 0.4 -1.1 0.4 -1.5 0 z"/>
+    <path fill="white" d="M 4.5 9.2 c -0.4 0 -0.7 -0.3 -0.7 -0.7 v -1 c 0 -0.4 0.3 -0.7 0.7 -0.7 h 7 c 0.4 0 0.7 0.3 0.7 0.7 v 1 c 0 0.4 -0.3 0.7 -0.7 0.7 h -7 z"/>
+    <path fill="rgb(153, 153, 153)" d="M 11.5 7 c 0.3 0 0.5 0.2 0.5 0.5 v 1 c 0 0.3 -0.2 0.5 -0.5 0.5 h -7 c -0.3 0 -0.5 -0.2 -0.5 -0.5 v -1 c 0 -0.3 0.2 -0.5 0.5 -0.5 h 7 m 0 -0.3 h -7 c -0.5 0 -0.8 0.3 -0.8 0.8 v 1 c 0 0.5 0.3 0.8 0.8 0.8 h 7 c 0.5 0 0.8 -0.4 0.8 -0.8 v -1 c 0 -0.5 -0.3 -0.8 -0.8 -0.8 z"/>
+</svg>
diff --git a/Source/WebInspectorUI/UserInterface/Images/AuditTestWarn.svg b/Source/WebInspectorUI/UserInterface/Images/AuditTestWarn.svg
new file mode 100644 (file)
index 0000000..2448264
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright © 2018 Apple Inc. All rights reserved. -->
+<svg xmlns="http://www.w3.org/2000/svg" id="root" version="1.1" viewBox="0 0 16 16">
+    <path fill="rgb(243, 222, 156)" stroke="rgb(201, 181, 118)" stroke-width="0.5" stroke-miterlimit="10" d="M 7.3 14.7 l -6 -6 c -0.4 -0.4 -0.4 -1.1 0 -1.5 l 6 -6 c 0.4 -0.4 1.1 -0.4 1.5 0 l 6 6 c 0.4 0.4 0.4 1.1 0 1.5 l -6 6 c -0.5 0.4 -1.1 0.4 -1.5 0 z"/>
+    <circle fill="white" cx="8" cy="11.5" r="1.415"/>
+    <path fill="rgb(201, 181, 118)" d="M 8 10.25 a 1.25 1.25 0 1 1 0 2.5 1.25 1.25 0 0 1 0 -2.5 m 0 -0.33 c -0.871 0 -1.58 0.709 -1.58 1.58 s 0.709 1.58 1.58 1.58 1.58 -0.709 1.58 -1.58 S 8.871 9.92 8 9.92 z"/>
+    <path fill="white" d="M 8 9.165 A 1.166 1.166 0 0 1 6.835 8 V 4 a 1.166 1.166 0 0 1 2.33 0 v 4 c 0 0.643 -0.522 1.165 -1.165 1.165 z"/>
+    <path fill="rgb(201, 181, 118)" d="M 8 3 c 0.55 0 1 0.45 1 1 v 4 c 0 0.55 -0.45 1 -1 1 s -1 -0.45 -1 -1 V 4 c 0 -0.55 0.45 -1 1 -1 m 0 -0.33 c -0.733 0 -1.33 0.597 -1.33 1.33 v 4 c 0 0.733 0.597 1.33 1.33 1.33 S 9.33 8.733 9.33 8 V 4 c 0 -0.733 -0.597 -1.33 -1.33 -1.33 z"/>
+</svg>
index fb8013a..ff76ca5 100644 (file)
     <link rel="stylesheet" href="External/CodeMirror/codemirror.css">
 
     <link rel="stylesheet" href="Views/ApplicationCacheFrameContentView.css">
+    <link rel="stylesheet" href="Views/AuditNavigationSidebarPanel.css">
+    <link rel="stylesheet" href="Views/AuditTestCaseContentView.css">
+    <link rel="stylesheet" href="Views/AuditTestContentView.css">
+    <link rel="stylesheet" href="Views/AuditTestGroupContentView.css">
+    <link rel="stylesheet" href="Views/AuditTreeElement.css">
     <link rel="stylesheet" href="Views/BezierEditor.css">
     <link rel="stylesheet" href="Views/BoxModelDetailsSectionRow.css">
     <link rel="stylesheet" href="Views/BreakpointActionView.css">
     <script src="Models/WrappedPromise.js"></script>
     <script src="Models/XHRBreakpoint.js"></script>
 
-    <script src="Models/AuditReport.js"></script>
-    <script src="Models/AuditResult.js"></script>
+    <script src="Models/AuditTestBase.js"></script>
     <script src="Models/AuditTestCase.js"></script>
-    <script src="Models/AuditTestSuite.js"></script>
+    <script src="Models/AuditTestGroup.js"></script>
+    <script src="Models/AuditTestResultBase.js"></script>
+    <script src="Models/AuditTestCaseResult.js"></script>
+    <script src="Models/AuditTestGroupResult.js"></script>
 
     <script src="Proxies/FormatterWorkerProxy.js"></script>
     <script src="Proxies/HeapSnapshotDiffProxy.js"></script>
     <script src="Views/ComputedStyleDetailsSidebarPanel.js"></script>
     <script src="Views/StyleDetailsPanel.js"></script>
 
+    <script src="Views/AuditTabContentView.js"></script>
     <script src="Views/CanvasTabContentView.js"></script>
     <script src="Views/ConsoleTabContentView.js"></script>
     <script src="Views/DebuggerTabContentView.js"></script>
     <script src="Views/ApplicationCacheFrameContentView.js"></script>
     <script src="Views/ApplicationCacheFrameTreeElement.js"></script>
     <script src="Views/ApplicationCacheManifestTreeElement.js"></script>
+    <script src="Views/AuditNavigationSidebarPanel.js"></script>
+    <script src="Views/AuditTestContentView.js"></script>
+    <script src="Views/AuditTestCaseContentView.js"></script>
+    <script src="Views/AuditTestGroupContentView.js"></script>
+    <script src="Views/AuditTreeElement.js"></script>
     <script src="Views/BezierEditor.js"></script>
     <script src="Views/BoxModelDetailsSectionRow.js"></script>
     <script src="Views/BreakpointActionView.js"></script>
diff --git a/Source/WebInspectorUI/UserInterface/Models/AuditReport.js b/Source/WebInspectorUI/UserInterface/Models/AuditReport.js
deleted file mode 100644 (file)
index 62ad09d..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright (C) 2018 Apple Inc. All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- * 1. Redistributions of source code must retain the above copyright
- *    notice, this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright
- *    notice, this list of conditions and the following disclaimer in the
- *    documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
- * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
- * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
- * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
- * THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-WI.AuditReport = class AuditReport
-{
-    constructor(representedTest)
-    {
-        console.assert(representedTest instanceof WI.AuditTestCase || representedTest instanceof WI.AuditTestSuite);
-
-        this._results = [];
-        this._isWritable = true;
-        this._representedTestCases = (representedTest instanceof WI.AuditTestCase) ? [representedTest] : [...representedTest.testCases];
-        this._representedTestSuite = (representedTest instanceof WI.AuditTestSuite) ? representedTest : null;
-    }
-
-    // Public
-
-    get representedTestCases() { return this._representedTestCases.slice(); }
-    get representedTestSuite() { return this._representedTestSuite; }
-    get resultsData() { return this._results.slice(); }
-    get isWritable() { return this._isWritable; }
-
-    get failedCount() {
-        return this._results.slice().filter(result => result.failed).length;
-    }
-
-    addResult(auditResult)
-    {
-        if (!this._isWritable)
-            return;
-
-        console.assert(auditResult instanceof WI.AuditResult);
-        this._results.push(auditResult);
-    }
-
-    close()
-    {
-        this._isWritable = false;
-    }
-};
diff --git a/Source/WebInspectorUI/UserInterface/Models/AuditTestBase.js b/Source/WebInspectorUI/UserInterface/Models/AuditTestBase.js
new file mode 100644 (file)
index 0000000..04dfdbc
--- /dev/null
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2018 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+WI.AuditTestBase = class AuditTestBase extends WI.Object
+{
+    constructor(name, {description} = {})
+    {
+        console.assert(typeof name === "string");
+        console.assert(!description || typeof description === "string");
+
+        super();
+
+        this._name = name;
+        this._description = description || null;
+
+        this._runningState = WI.AuditManager.RunningState.Inactive;
+        this._result = null;
+    }
+
+    // Public
+
+    get name() { return this._name; }
+    get description() { return this._description; }
+    get runningState() { return this._runningState; }
+    get result() { return this._result; }
+
+    async start()
+    {
+        // Called from WI.AuditManager.
+
+        console.assert(WI.auditManager.runningState === WI.AuditManager.RunningState.Active);
+
+        console.assert(this._runningState === WI.AuditManager.RunningState.Inactive);
+        if (this._runningState !== WI.AuditManager.RunningState.Inactive)
+            return;
+
+        this._runningState = WI.AuditManager.RunningState.Active;
+        this.dispatchEventToListeners(WI.AuditTestBase.Event.Scheduled);
+
+        await this.run();
+
+        this._runningState = WI.AuditManager.RunningState.Inactive;
+        this.dispatchEventToListeners(WI.AuditTestBase.Event.Completed);
+    }
+
+    stop()
+    {
+        // Called from WI.AuditManager.
+
+        console.assert(this._runningState !== WI.AuditManager.RunningState.Inactive);
+
+        if (this._runningState !== WI.AuditManager.RunningState.Active)
+            return;
+
+        this._runningState = WI.AuditManager.RunningState.Stopping;
+        this.dispatchEventToListeners(WI.AuditTestBase.Event.Stopping);
+    }
+
+    clearResult(options = {})
+    {
+        if (!this._result)
+            return false;
+
+        this._result = null;
+
+        if (!options.suppressResultClearedEvent)
+            this.dispatchEventToListeners(WI.AuditTestBase.Event.ResultCleared);
+
+        return true;
+    }
+
+    saveIdentityToCookie(cookie)
+    {
+        cookie["audit-" + this.constructor.TypeIdentifier + "-name"] = this._name;
+    }
+
+    toJSON()
+    {
+        let json = {
+            type: this.constructor.TypeIdentifier,
+            name: this._name,
+        };
+        if (this._description)
+            json.description = this._description;
+        return json;
+    }
+
+    // Protected
+
+    async run()
+    {
+        throw WI.NotImplementedError.subclassMustOverride();
+    }
+};
+
+WI.AuditTestBase.Event = {
+    Completed: "audit-test-base-completed",
+    Progress: "audit-test-base-progress",
+    ResultCleared: "audit-test-base-result-cleared",
+    Scheduled: "audit-test-base-scheduled",
+    Stopping: "audit-test-base-stopping",
+};
index d73800d..0eee197 100644 (file)
  * THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-WI.AuditTestCase = class AuditTestCase extends WI.Object
+WI.AuditTestCase = class AuditTestCase extends WI.AuditTestBase
 {
-    constructor(suite, name, test, setup, tearDown, errorDetails = {})
+    constructor(name, test, {description} = {})
     {
-        console.assert(suite instanceof WI.AuditTestSuite);
-        console.assert(typeof name === "string");
+        console.assert(typeof test === "string");
 
-        if (setup)
-            console.assert(setup instanceof Function);
+        super(name, {description});
 
-        if (tearDown)
-            console.assert(tearDown instanceof Function);
+        this._test = test;
+    }
+
+    // Static
 
-        if (test[Symbol.toStringTag] !== "AsyncFunction")
-            throw new Error("Test functions must be async functions.");
+    static fromPayload(payload)
+    {
+        if (typeof payload !== "object" || payload === null)
+            return null;
 
-        super();
-        this._id = Symbol(name);
+        let {type, name, test, description} = payload;
 
-        this._suite = suite;
-        this._name = name;
-        this._test = test;
-        this._setup = setup;
-        this._tearDown = tearDown;
-        this._errorDetails = errorDetails;
+        if (type !== WI.AuditTestCase.TypeIdentifier)
+            return null;
+
+        if (typeof name !== "string")
+            return null;
+
+        if (typeof test !== "string")
+            return null;
+
+        let options = {};
+        if (typeof description === "string")
+            options.description = description;
+
+        return new WI.AuditTestCase(name, test, options);
     }
 
     // Public
 
-    get id() { return this._id; }
-    get name() { return this._name; }
-    get suite() { return this._suite; }
     get test() { return this._test; }
-    get setup() { return this._setup; }
-    get tearDown() { return this._tearDown; }
-    get errorDetails() { return this._errorDetails; }
+
+    toJSON()
+    {
+        let json = super.toJSON();
+        json.test = this._test;
+        return json;
+    }
+
+    // Protected
+
+    async run()
+    {
+        const levelStrings = Object.values(WI.AuditTestCaseResult.Level);
+        let level = null;
+        let data = {};
+
+        function setLevel(newLevel) {
+            let newLevelIndex = levelStrings.indexOf(newLevel);
+            if (newLevelIndex < 0) {
+                addError(WI.UIString("Return string must be one of %s").format(JSON.stringify(levelStrings)));
+                return;
+            }
+
+            if (newLevelIndex <= levelStrings.indexOf(level))
+                return;
+
+            level = newLevel;
+        }
+
+        function addError(value) {
+            setLevel(WI.AuditTestCaseResult.Level.Error);
+
+            if (!data.errors)
+                data.errors = [];
+
+            data.errors.push(value);
+        }
+
+        try {
+            let {result, wasThrown} = await RuntimeAgent.evaluate.invoke({
+                expression: `(function() { "use strict"; return eval(${this._test})(); })()`,
+                objectGroup: "audit",
+                doNotPauseOnExceptionsAndMuteConsole: true,
+            });
+            let remoteObject = WI.RemoteObject.fromPayload(result, WI.mainTarget);
+
+            if (wasThrown || (remoteObject.type === "object" && remoteObject.subtype === "error"))
+                addError(remoteObject.description);
+            else if (remoteObject.type === "boolean")
+                setLevel(remoteObject.value ? WI.AuditTestCaseResult.Level.Pass : WI.AuditTestCaseResult.Level.Fail);
+            else if (remoteObject.type === "string")
+                setLevel(remoteObject.value.trim().toLowerCase());
+            else if (remoteObject.type === "object" && !remoteObject.subtype) {
+                const options = {
+                    ownProperties: true,
+                };
+
+                let properties = await new Promise((resolve, reject) => remoteObject.getPropertyDescriptorsAsObject(resolve, options));
+
+                function checkResultProperty(key, type, subtype) {
+                    if (!(key in properties))
+                        return null;
+
+                    let property = properties[key].value;
+                    if (!property)
+                        return null;
+
+                    function addErrorForValueType(valueType) {
+                        let value = null;
+                        if (valueType === "object" || valueType === "array")
+                            value = WI.UIString("“%s“ must be an %s");
+                        else
+                            value = WI.UIString("“%s“ must be a %s");
+                        addError(value.format(key, valueType));
+                    }
+
+                    if (property.subtype !== subtype) {
+                        addErrorForValueType(subtype);
+                        return null;
+                    }
+
+                    if (property.type !== type) {
+                        addErrorForValueType(type);
+                        return null;
+                    }
+
+                    if (type === "boolean" || type === "string")
+                        return property.value;
+
+                    return property;
+                }
+
+                async function resultArrayForEach(key, callback) {
+                    let array = checkResultProperty(key, "object", "array");
+                    if (!array)
+                        return;
+
+                    // `getPropertyDescriptorsAsObject` returns an object, meaning that if we
+                    // want to iterate over `array` by index, we have to count.
+                    let asObject = await new Promise((resolve, reject) => array.getPropertyDescriptorsAsObject(resolve, options));
+                    for (let i = 0; i < array.size; ++i) {
+                        if (i in asObject)
+                            await callback(asObject[i]);
+                    }
+                }
+
+                let levelString = checkResultProperty("level", "string");
+                if (levelString)
+                    setLevel(levelString.trim().toLowerCase());
+
+                if (checkResultProperty("pass", "boolean"))
+                    setLevel(WI.AuditTestCaseResult.Level.Pass);
+                if (checkResultProperty("warn", "boolean"))
+                    setLevel(WI.AuditTestCaseResult.Level.Warn);
+                if (checkResultProperty("fail", "boolean"))
+                    setLevel(WI.AuditTestCaseResult.Level.Fail);
+                if (checkResultProperty("error", "boolean"))
+                    setLevel(WI.AuditTestCaseResult.Level.Error);
+                if (checkResultProperty("unsupported", "boolean"))
+                    setLevel(WI.AuditTestCaseResult.Level.Unsupported);
+
+                await resultArrayForEach("domNodes", async (item) => {
+                    if (!item || !item.value || item.value.type !== "object" || item.value.subtype !== "node") {
+                        addError(WI.UIString("All items in “%s“ must be valid DOM nodes").format(WI.unlocalizedString("domNodes")));
+                        return;
+                    }
+
+                    let domNodeId = await new Promise((resolve, reject) => item.value.pushNodeToFrontend(resolve));
+                    let domNode = WI.domManager.nodeForId(domNodeId);
+                    if (!domNode)
+                        return;
+
+                    if (!data.domNodes)
+                        data.domNodes = [];
+                    data.domNodes.push(domNode);
+                });
+
+                await resultArrayForEach("domAttributes", (item) => {
+                    if (!item || !item.value || item.value.type !== "string" || !item.value.value.length) {
+                        addError(WI.UIString("All items in “%s“ must be non-empty strings").format(WI.unlocalizedString("domAttributes")));
+                        return;
+                    }
+
+                    if (!data.domAttributes)
+                        data.domAttributes = [];
+                    data.domAttributes.push(item.value.value);
+                });
+
+                await resultArrayForEach("errors", (item) => {
+                    if (!item || !item.value || item.value.type !== "object" || item.value.subtype !== "error") {
+                        addError(WI.UIString("All items in “%s“ must be error objects").format(WI.unlocalizedString("errors")));
+                        return;
+                    }
+
+                    addError(item.value.description);
+                });
+            } else
+                addError(WI.UIString("Return value is not an object, string, or boolean"));
+        } catch (error) {
+            addError(error.message);
+        }
+
+        if (!level)
+            addError(WI.UIString("Missing result level"));
+
+        let options = {
+            description: this.description,
+        };
+        if (!isEmptyObject(data))
+            options.data = data;
+        this._result = new WI.AuditTestCaseResult(this.name, level, options);
+    }
 };
+
+WI.AuditTestCase.TypeIdentifier = "test-case";
diff --git a/Source/WebInspectorUI/UserInterface/Models/AuditTestCaseResult.js b/Source/WebInspectorUI/UserInterface/Models/AuditTestCaseResult.js
new file mode 100644 (file)
index 0000000..397f183
--- /dev/null
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2018 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+WI.AuditTestCaseResult = class AuditTestCaseResult extends WI.AuditTestResultBase
+{
+    constructor(name, level, {data, description} = {})
+    {
+        console.assert(Object.values(WI.AuditTestCaseResult.Level).includes(level));
+        console.assert(!data || typeof data === "object");
+
+        super(name, {description});
+
+        this._level = level;
+        this._data = data || {};
+    }
+
+    // Static
+
+    static fromPayload(payload)
+    {
+        if (typeof payload !== "object" || payload === null)
+            return null;
+
+        let {type, name, description, level, data} = payload;
+
+        if (type !== WI.AuditTestCaseResult.TypeIdentifier)
+            return null;
+
+        if (typeof name !== "string")
+            return null;
+
+        if (!Object.values(WI.AuditTestCaseResult.Level).includes(level))
+            return null;
+
+        if (typeof data !== "object" || data === null)
+            data = {};
+
+        function checkArray(key) {
+            if (!data[key])
+                return;
+
+            if (!Array.isArray(data[key]))
+                data[key] = [];
+
+            data[key] = data[key].filter((item) => typeof item === "string");
+        }
+        checkArray("domNodes");
+        checkArray("domAttributes");
+        checkArray("errors");
+
+        let options = {};
+        if (typeof description === "string")
+            options.description = description;
+        if (!isEmptyObject(data))
+            options.data = data;
+        return new WI.AuditTestCaseResult(name, level, options);
+    }
+
+    // Public
+
+    get level() { return this._level; }
+    get data() { return this._data; }
+
+    get result()
+    {
+        return this;
+    }
+
+    get didPass()
+    {
+        return this._level === WI.AuditTestCaseResult.Level.Pass;
+    }
+
+    get didWarn()
+    {
+        return this._level === WI.AuditTestCaseResult.Level.Warn;
+    }
+
+    get didFail()
+    {
+        return this._level === WI.AuditTestCaseResult.Level.Fail;
+    }
+
+    get didError()
+    {
+        return this._level === WI.AuditTestCaseResult.Level.Error;
+    }
+
+    get unsupported()
+    {
+        return this._level === WI.AuditTestCaseResult.Level.Unsupported;
+    }
+
+    toJSON()
+    {
+        let json = super.toJSON();
+        json.level = this._level;
+
+        let data = {};
+        if (this._data.domNodes && this._data.domNodes.length) {
+            data.domNodes = this._data.domNodes.map((domNode) => domNode instanceof WI.DOMNode ? WI.cssPath(domNode, {full: true}) : domNode);
+            if (this._data.domAttributes && this._data.domAttributes.length)
+                data.domAttributes = this._data.domAttributes;
+        }
+        if (this._data.errors && this._data.errors.length)
+            data.errors = this._data.errors;
+        if (!isEmptyObject(data))
+            json.data = data;
+
+        return json;
+    }
+};
+
+WI.AuditTestCaseResult.TypeIdentifier = "test-case-result";
+
+// Keep this ordered by precedence.
+WI.AuditTestCaseResult.Level = {
+    Pass: "pass",
+    Warn: "warn",
+    Fail: "fail",
+    Error: "error",
+    Unsupported: "unsupported",
+};
diff --git a/Source/WebInspectorUI/UserInterface/Models/AuditTestGroup.js b/Source/WebInspectorUI/UserInterface/Models/AuditTestGroup.js
new file mode 100644 (file)
index 0000000..784e974
--- /dev/null
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2018 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+WI.AuditTestGroup = class AuditTestGroup extends WI.AuditTestBase
+{
+    constructor(name, tests, {description} = {})
+    {
+        console.assert(Array.isArray(tests));
+
+        super(name, {description});
+
+        this._tests = tests;
+
+        for (let test of this._tests) {
+            test.addEventListener(WI.AuditTestBase.Event.Completed, this._handleTestCompleted, this);
+            test.addEventListener(WI.AuditTestBase.Event.Progress, this._handleTestProgress, this);
+        }
+    }
+
+    // Static
+
+    static fromPayload(payload)
+    {
+        if (typeof payload !== "object" || payload === null)
+            return null;
+
+        let {type, name, tests, description} = payload;
+
+        if (type !== WI.AuditTestGroup.TypeIdentifier)
+            return null;
+
+        if (typeof name !== "string")
+            return null;
+
+        if (!Array.isArray(tests))
+            return null;
+
+        tests = tests.map((test) => WI.AuditTestCase.fromPayload(test) || WI.AuditTestGroup.fromPayload(test));
+        tests = tests.filter((test) => !!test);
+        if (!tests.length)
+            return null;
+
+        let options = {};
+        if (typeof description === "string")
+            options.description = description;
+
+        return new WI.AuditTestGroup(name, tests, options);
+    }
+
+    // Public
+
+    get tests() { return this._tests; }
+
+    stop()
+    {
+        // Called from WI.AuditManager.
+
+        for (let test of this._tests)
+            test.stop();
+
+        super.stop();
+    }
+
+    clearResult(options = {})
+    {
+        let cleared = !!this._result;
+        for (let test of this._tests) {
+            if (test.clearResult(options))
+                cleared = true;
+        }
+
+        return super.clearResult({
+            ...options,
+            suppressResultClearedEvent: !cleared,
+        });
+    }
+
+    toJSON()
+    {
+        let json = super.toJSON();
+        json.tests = this._tests.map((testCase) => testCase.toJSON());
+        return json;
+    }
+
+    // Protected
+
+    async run()
+    {
+        let count = this._tests.length;
+        for (let index = 0; index < count && this._runningState === WI.AuditManager.RunningState.Active; ++index) {
+            let test = this._tests[index];
+
+            await test.start();
+
+            if (test instanceof WI.AuditTestCase)
+                this.dispatchEventToListeners(WI.AuditTestBase.Event.Progress, {index, count});
+        }
+
+        this._updateResult();
+    }
+
+    // Private
+
+    _updateResult()
+    {
+        let results = this._tests.map((test) => test.result).filter((result) => !!result);
+        if (!results.length)
+            return;
+
+        this._result = new WI.AuditTestGroupResult(this.name, results, {
+            description: this.description,
+        });
+    }
+
+    _handleTestCompleted(event)
+    {
+        if (this._runningState === WI.AuditManager.RunningState.Active)
+            return;
+
+        this._updateResult();
+        this.dispatchEventToListeners(WI.AuditTestBase.Event.Completed);
+    }
+
+    _handleTestProgress(event)
+    {
+        if (this._runningState !== WI.AuditManager.RunningState.Active)
+            return;
+
+        let walk = (tests) => {
+            let count = 0;
+            for (let test of tests) {
+                if (test instanceof WI.AuditTestCase)
+                    ++count;
+                else if (test instanceof WI.AuditTestGroup)
+                    count += walk(test.tests);
+            }
+            return count;
+        };
+
+        this.dispatchEventToListeners(WI.AuditTestBase.Event.Progress, {
+            index: event.data.index + walk(this.tests.slice(0, this.tests.indexOf(event.target))),
+            count: walk(this.tests),
+        });
+    }
+};
+
+WI.AuditTestGroup.TypeIdentifier = "test-group";
diff --git a/Source/WebInspectorUI/UserInterface/Models/AuditTestGroupResult.js b/Source/WebInspectorUI/UserInterface/Models/AuditTestGroupResult.js
new file mode 100644 (file)
index 0000000..012b463
--- /dev/null
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2018 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+WI.AuditTestGroupResult = class AuditTestGroupResult extends WI.AuditTestResultBase
+{
+    constructor(name, results, {description} = {})
+    {
+        console.assert(Array.isArray(results));
+
+        super(name, {description});
+
+        this._results = results;
+    }
+
+    // Static
+
+    static fromPayload(payload)
+    {
+        if (typeof payload !== "object" || payload === null)
+            return null;
+
+        let {type, name, description, results} = payload;
+
+        if (type !== WI.AuditTestGroupResult.TypeIdentifier)
+            return null;
+
+        if (typeof name !== "string")
+            return null;
+
+        if (!Array.isArray(results))
+            return null;
+
+        results = results.map((result) => WI.AuditTestGroupResult.fromPayload(result) || WI.AuditTestCaseResult.fromPayload(result));
+        results = results.filter((result) => !!result);
+        if (!results.length)
+            return null;
+
+        let options = {};
+        if (typeof description === "string")
+            options.description = description;
+
+        return new WI.AuditTestGroupResult(name, results, options);
+    }
+
+    // Public
+
+    get results() { return this._results; }
+
+    get levelCounts()
+    {
+        let counts = {};
+        for (let level of Object.values(WI.AuditTestCaseResult.Level))
+            counts[level] = 0;
+
+        for (let result of this._results) {
+            if (result instanceof WI.AuditTestCaseResult)
+                ++counts[result.level];
+            else if (result instanceof WI.AuditTestGroupResult) {
+                for (let [level, count] of Object.entries(result.levelCounts))
+                    counts[level] += count;
+            }
+        }
+
+        return counts;
+    }
+
+    get didPass()
+    {
+        return this._results.some((result) => result.didPass);
+    }
+
+    get didWarn()
+    {
+        return this._results.some((result) => result.didWarn);
+    }
+
+    get didFail()
+    {
+        return this._results.some((result) => result.didFail);
+    }
+
+    get didError()
+    {
+        return this._results.some((result) => result.didError);
+    }
+
+    get unsupported()
+    {
+        return this._results.some((result) => result.unsupported);
+    }
+
+    toJSON()
+    {
+        let json = super.toJSON();
+        json.results = this._results.map((result) => result.toJSON());
+        return json;
+    }
+};
+
+WI.AuditTestGroupResult.TypeIdentifier = "test-group-result";
  * THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-WI.AuditTestSuite = class AuditTestSuite extends WI.Object
+WI.AuditTestResultBase = class AuditTestResultBase
 {
-    constructor(identifier, name)
+    constructor(name, {description} = {})
     {
-        super();
-        this._id = Symbol(identifier);
-        this._name = name;
-        this._testCases = new Map;
+        console.assert(typeof name === "string");
+        console.assert(!description || typeof description === "string");
 
-        this._buildTestCasesFromDescriptors();
+        this._name = name;
+        this._description = description || null;
     }
 
-    static testCaseDescriptors() { throw WI.NotImplementedError.subclassMustOverride(); }
-
     // Public
 
-    get id() { return this._id; }
     get name() { return this._name; }
-    get testCases() {
-        return [...this._testCases.values()];
+    get description() { return this._description; }
+
+    get result()
+    {
+        return this;
     }
 
-    // Private
+    get didPass()
+    {
+        throw WI.NotImplementedError.subclassMustOverride();
+    }
 
-    _buildTestCasesFromDescriptors()
+    get didWarn()
     {
-        for (let descriptor of this.constructor.testCaseDescriptors()) {
-            if (typeof descriptor.name !== "string" || !descriptor.name)
-                throw new Error("Test name must be a valid string.");
+        throw WI.NotImplementedError.subclassMustOverride();
+    }
 
-            let {name, test, setup, tearDown, errorDetails} = descriptor;
+    get didFail()
+    {
+        throw WI.NotImplementedError.subclassMustOverride();
+    }
 
-            if (!(test instanceof Function) || test[Symbol.toStringTag] !== "AsyncFunction")
-                throw new Error("Test function must be an async function.");
+    get didError()
+    {
+        throw WI.NotImplementedError.subclassMustOverride();
+    }
 
-            let testCaseInstance = new WI.AuditTestCase(this, name, test, setup, tearDown, errorDetails);
+    get unsupported()
+    {
+        throw WI.NotImplementedError.subclassMustOverride();
+    }
 
-            this._testCases.set(testCaseInstance.id, testCaseInstance);
-        }
+    saveIdentityToCookie(cookie)
+    {
+        cookie["audit-" + this.constructor.TypeIdentifier + "-name"] = this._name;
     }
-};
 
-WI.AuditTestSuite.Event = {
-    NewAuditResultAvailable: Symbol("new-audit-result-available")
+    toJSON()
+    {
+        let json = {
+            type: this.constructor.TypeIdentifier,
+            name: this._name,
+        };
+        if (this._description)
+            json.description = this._description;
+        return json;
+    }
 };
index 9a97567..75dc0b2 100644 (file)
     <script src="Models/WrappedPromise.js"></script>
     <script src="Models/XHRBreakpoint.js"></script>
 
-    <script src="Models/AuditReport.js"></script>
-    <script src="Models/AuditResult.js"></script>
+    <script src="Models/AuditTestBase.js"></script>
     <script src="Models/AuditTestCase.js"></script>
-    <script src="Models/AuditTestSuite.js"></script>
+    <script src="Models/AuditTestGroup.js"></script>
+    <script src="Models/AuditTestResultBase.js"></script>
+    <script src="Models/AuditTestCaseResult.js"></script>
+    <script src="Models/AuditTestGroupResult.js"></script>
 
     <script src="Proxies/FormatterWorkerProxy.js"></script>
     <script src="Proxies/HeapSnapshotDiffProxy.js"></script>
index 1acba8b..e012c1d 100644 (file)
@@ -63,6 +63,7 @@ WI.loaded = function()
     this.workerManager = new WI.WorkerManager;
     this.domDebuggerManager = new WI.DOMDebuggerManager;
     this.canvasManager = new WI.CanvasManager;
+    this.auditManager = new WI.AuditManager;
 
     document.addEventListener("DOMContentLoaded", this.contentLoaded);
 
  * THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-WI.AuditResult = class AuditResult
-{
-    constructor(testInstance, testResult, failed)
-    {
-        this._testResult = testResult;
-        this._failed = failed || false;
-        this._testName = testInstance.name;
-        this._errorDetails = testInstance.errorDetails;
-        this._logLevel = this._errorDetails.logLevel || WI.AuditResult.LogLevel.Passed;
-        this._errorTitle = this._errorDetails.title;
-        this._hint = this._errorDetails.hint;
-        this._documentation = this._errorDetails.documentation;
-    }
-
-    // Public
-
-    get testResult() { return this._testResult; }
-    get name() { return this._testName; }
-    get logLevel() { return this._logLevel; }
-    get failed() { return this._failed; }
-};
-
-WI.AuditResult.LogLevel = {
-    Error: "error",
-    Warning: "warning",
-    Passed: "passed"
-};
+.sidebar > .panel.navigation.audit > .content {
+    top: var(--navigation-bar-height);
+}
diff --git a/Source/WebInspectorUI/UserInterface/Views/AuditNavigationSidebarPanel.js b/Source/WebInspectorUI/UserInterface/Views/AuditNavigationSidebarPanel.js
new file mode 100644 (file)
index 0000000..1df8a99
--- /dev/null
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2018 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+WI.AuditNavigationSidebarPanel = class AuditNavigationSidebarPanel extends WI.NavigationSidebarPanel
+{
+    constructor()
+    {
+        super("audit", WI.UIString("Audits"));
+    }
+
+    // Public
+
+    showDefaultContentView()
+    {
+        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, () => {
+            WI.auditManager.import();
+        });
+
+        let importHelpElement = WI.createNavigationItemHelp(WI.UIString("Press %s to import a test or result file"), importNavigationItem);
+        contentPlaceholder.appendChild(importHelpElement);
+
+        this.contentBrowser.showContentView(contentView);
+    }
+
+    // Protected
+
+    initialLayout()
+    {
+        super.initialLayout();
+
+        this.contentTreeOutline.allowsRepeatSelection = false;
+
+        this._resultsFolderTreeElement = new WI.FolderTreeElement(WI.UIString("Results"));
+        this.contentTreeOutline.appendChild(this._resultsFolderTreeElement);
+        this._resultsFolderTreeElement.hidden = true;
+        this._resultsFolderTreeElement.expand();
+
+        let navigationBar = 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);
+
+        navigationBar.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);
+
+        this.addSubview(navigationBar);
+
+        for (let test of WI.auditManager.tests)
+            this._addTest(test);
+
+        for (let result of WI.auditManager.results)
+            this._addResult(result);
+
+        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.TestScheduled, this._handleAuditTestScheduled, this);
+
+        this.contentTreeOutline.addEventListener(WI.TreeOutline.Event.SelectionDidChange, this._treeSelectionDidChange, this);
+    }
+
+    closed()
+    {
+        super.closed();
+
+        WI.auditManager.removeEventListener(null, null, this);
+    }
+
+    // Private
+
+    _addTest(test)
+    {
+        this._updateStartStopButtonNavigationItemState();
+
+        this.contentTreeOutline.insertChild(new WI.AuditTreeElement(test), this.contentTreeOutline.children.indexOf(this._resultsFolderTreeElement));
+
+        this._resultsFolderTreeElement.hidden = !this._resultsFolderTreeElement.children.length;
+    }
+
+    _addResult(result, index)
+    {
+        this._updateStartStopButtonNavigationItemState();
+
+        this._resultsFolderTreeElement.hidden = false;
+
+        let resultFolderTreeElement = new WI.FolderTreeElement(WI.UIString("Run %d").format(index + 1));
+        if (result instanceof WI.AuditTestResultBase) {
+            resultFolderTreeElement.subtitle = WI.UIString("Imported");
+            result = [result];
+        }
+        this._resultsFolderTreeElement.appendChild(resultFolderTreeElement);
+
+        for (let resultItem of result)
+            resultFolderTreeElement.appendChild(new WI.AuditTreeElement(resultItem));
+    }
+
+    _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;
+    }
+
+    _handleAuditTestAdded(event)
+    {
+        this._addTest(event.data.test);
+    }
+
+    _handleAuditTestCompleted(event)
+    {
+        let {result, index} = event.data;
+        this._addResult(result, index);
+    }
+
+    _handleAuditTestScheduled(event)
+    {
+        this._updateStartStopButtonNavigationItemState();
+    }
+
+    _treeSelectionDidChange(event)
+    {
+        if (!this.selected)
+            return;
+
+        let treeElement = event.data.selectedElement;
+        if (!treeElement || treeElement instanceof WI.FolderTreeElement) {
+            this.showDefaultContentView();
+            return;
+        }
+
+        let representedObject = treeElement.representedObject;
+        if (representedObject instanceof WI.AuditTestCase || representedObject instanceof WI.AuditTestGroup
+            || representedObject instanceof WI.AuditTestCaseResult || representedObject instanceof WI.AuditTestGroupResult) {
+            WI.showRepresentedObject(representedObject);
+            return;
+        }
+
+        console.error("Unknown tree element", treeElement);
+    }
+
+    _handleStartStopButtonNavigationItemClicked(event)
+    {
+        if (WI.auditManager.runningState === WI.AuditManager.RunningState.Inactive)
+            WI.auditManager.start();
+        else if (WI.auditManager.runningState === WI.AuditManager.RunningState.Active)
+            WI.auditManager.stop();
+
+        this._updateStartStopButtonNavigationItemState();
+    }
+
+    _handleImportButtonNavigationItemClicked(event)
+    {
+        WI.auditManager.import();
+    }
+};
diff --git a/Source/WebInspectorUI/UserInterface/Views/AuditTabContentView.js b/Source/WebInspectorUI/UserInterface/Views/AuditTabContentView.js
new file mode 100644 (file)
index 0000000..29f481a
--- /dev/null
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2018 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+WI.AuditTabContentView = class AuditTabContentView extends WI.ContentBrowserTabContentView
+{
+    constructor()
+    {
+        super("audit", ["audit"], WI.GeneralTabBarItem.fromTabInfo(WI.AuditTabContentView.tabInfo()), WI.AuditNavigationSidebarPanel);
+
+        this._startStopShortcut = new WI.KeyboardShortcut(null, WI.KeyboardShortcut.Key.Space, this._handleSpace.bind(this));
+        this._startStopShortcut.implicitlyPreventsDefault = false;
+        this._startStopShortcut.disabled = true;
+    }
+
+    // Static
+
+    static tabInfo()
+    {
+        return {
+            image: "Images/Audit.svg",
+            title: WI.UIString("Audit"),
+        };
+    }
+
+    static isTabAllowed()
+    {
+        return !!window.RuntimeAgent && WI.settings.experimentalEnableAuditTab.value;
+    }
+
+    // Public
+
+    get type()
+    {
+        return WI.AuditTabContentView.Type;
+    }
+
+    get supportsSplitContentBrowser()
+    {
+        return true;
+    }
+
+    canShowRepresentedObject(representedObject)
+    {
+        return representedObject instanceof WI.AuditTestCase
+            || representedObject instanceof WI.AuditTestGroup
+            || representedObject instanceof WI.AuditTestCaseResult
+            || representedObject instanceof WI.AuditTestGroupResult;
+    }
+
+    shown()
+    {
+        super.shown();
+
+        this._startStopShortcut.disabled = false;
+    }
+
+    hidden()
+    {
+        this._startStopShortcut.disabled = true;
+
+        super.hidden();
+    }
+
+    // Private
+
+    _handleSpace(event)
+    {
+        if (WI.isEventTargetAnEditableField(event))
+            return;
+
+        if (WI.auditManager.runningState === WI.AuditManager.RunningState.Inactive)
+            WI.auditManager.start(this.contentBrowser.currentRepresentedObjects);
+        else if (WI.auditManager.runningState === WI.AuditManager.RunningState.Active)
+            WI.auditManager.stop();
+        else
+            return;
+
+        event.preventDefault();
+    }
+};
+
+WI.AuditTabContentView.Type = "audit";
diff --git a/Source/WebInspectorUI/UserInterface/Views/AuditTestCaseContentView.css b/Source/WebInspectorUI/UserInterface/Views/AuditTestCaseContentView.css
new file mode 100644 (file)
index 0000000..4dc5145
--- /dev/null
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2018 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+.content-view-container > .content-view.audit-test-case > header {
+    position: -webkit-sticky;
+    top: -1px;
+    z-index: var(--z-index-header);
+    margin-top: -1px;
+    background-color: var(--audit-test-header-background-color);
+    border-top: 1px solid var(--border-color);
+    -webkit-backdrop-filter: blur(20px);
+}
+
+.content-view-container > .content-view.audit-test-case > section > :not(.message-text-view):first-child {
+    margin-top: var(--audit-test-vertical-space);
+}
+
+.content-view.audit-test-case > header > h1 {
+    display: flex;
+    align-items: center;
+}
+
+.content-view.audit-test-case > header > h1 > img {
+    width: 1em;
+    height: 1em;
+    min-width: 16px;
+    min-height: 16px;
+    -webkit-margin-end: 4px;
+}
+
+.content-view.audit-test-case > section > :not(.message-text-view) {
+    margin-right: var(--audit-test-horizontal-space);
+    margin-left: var(--audit-test-horizontal-space);
+}
+
+.content-view.audit-test-case > section > :not(.message-text-view):last-child {
+    margin-bottom: var(--audit-test-vertical-space);
+}
+
+.content-view.audit-test-case > section > :not(.message-text-view) + :not(.message-text-view) {
+    margin-top: var(--audit-test-vertical-space);
+}
+
+.content-view.audit-test-case > section h1 {
+    margin-bottom: 4px;
+}
+
+.content-view.audit-test-case > section table {
+    border-collapse: collapse;
+}
+
+.content-view.audit-test-case > section table > tr + tr > td {
+    padding-top: 2px;
+}
+
+.content-view.audit-test-case > section table > tr > td > :not(.tree-outline) {
+    -webkit-user-select: text;
+}
+
+.content-view.audit-test-case > section table > tr > td:first-child {
+    -webkit-padding-start: calc(8px - 0.375em);
+    font-family: -webkit-system-font, sans-serif;
+    font-size: 11px;
+    font-variant-numeric: tabular-nums;
+    text-align: end;
+    vertical-align: top;
+    color: var(--console-secondary-text-color);
+}
+
+.content-view.audit-test-case > section > .dom-nodes > table > tr > td:first-child {
+    position: relative;
+    top: -1px;
+}
+
+.content-view.audit-test-case > section .CodeMirror {
+    width: 100%;
+    height: auto;
+}
+
+.content-view.audit-test-case > section .mark {
+    background-color: hsla(53, 83%, 53%, 0.2);
+    border-bottom: 1px solid hsl(47, 82%, 60%);
+}
diff --git a/Source/WebInspectorUI/UserInterface/Views/AuditTestCaseContentView.js b/Source/WebInspectorUI/UserInterface/Views/AuditTestCaseContentView.js
new file mode 100644 (file)
index 0000000..1c5ef00
--- /dev/null
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2018 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+WI.AuditTestCaseContentView = class AuditTestCaseContentView extends WI.AuditTestContentView
+{
+    constructor(representedObject)
+    {
+        console.assert(representedObject instanceof WI.AuditTestCase || representedObject instanceof WI.AuditTestCaseResult);
+
+        super(representedObject);
+
+        this.element.classList.add("audit-test-case");
+    }
+
+    // Protected
+
+    initialLayout()
+    {
+        super.initialLayout();
+
+        let nameElement = this.headerView.element.appendChild(document.createElement("h1"));
+
+        this._resultImageElement = nameElement.appendChild(document.createElement("img"));
+
+        nameElement.appendChild(document.createTextNode(this.representedObject.name));
+
+        if (this.representedObject.description) {
+            let descriptionElement = this.headerView.element.appendChild(document.createElement("p"));
+            descriptionElement.textContent = this.representedObject.description;
+        }
+    }
+
+    layout()
+    {
+        if (this.layoutReason !== WI.View.LayoutReason.Dirty)
+            return;
+
+        super.layout();
+
+        this._resultImageElement.src = "Images/AuditTestNoResult.svg";
+
+        this.contentView.element.removeChildren();
+
+        let result = this.representedObject.result;
+        if (!result) {
+            if (this.representedObject.runningState === WI.AuditManager.RunningState.Inactive)
+                this.showNoResultPlaceholder();
+            else if (this.representedObject.runningState === WI.AuditManager.RunningState.Active)
+                this.showRunningPlaceholder();
+            else if (this.representedObject.runningState === WI.AuditManager.RunningState.Stopping)
+                this.showStoppingPlaceholder();
+
+            return;
+        }
+
+        if (result.didError)
+            this._resultImageElement.src = "Images/AuditTestError.svg";
+        else if (result.didFail)
+            this._resultImageElement.src = "Images/AuditTestFail.svg";
+        else if (result.didWarn)
+            this._resultImageElement.src = "Images/AuditTestWarn.svg";
+        else if (result.didPass)
+            this._resultImageElement.src = "Images/AuditTestPass.svg";
+        else if (result.unsupported)
+            this._resultImageElement.src = "Images/AuditTestUnsupported.svg";
+
+        let resultData = result.data;
+
+        if (resultData.domNodes && resultData.domNodes.length) {
+            let domNodesContainer = this.contentView.element.appendChild(document.createElement("div"));
+            domNodesContainer.classList.add("dom-nodes");
+
+            let domNodeText = domNodesContainer.appendChild(document.createElement("h1"));
+            domNodeText.textContent = WI.UIString("DOM Nodes:");
+
+            let tableContainer = domNodesContainer.appendChild(document.createElement("table"));
+
+            resultData.domNodes.forEach((domNode, index) => {
+                let rowElement = tableContainer.appendChild(document.createElement("tr"));
+
+                let indexElement = rowElement.appendChild(document.createElement("td"));
+                indexElement.textContent = index + 1;
+
+                let dataElement = rowElement.appendChild(document.createElement("td"));
+
+                if (domNode instanceof WI.DOMNode) {
+                    let treeOutline = new WI.DOMTreeOutline;
+                    treeOutline.setVisible(true);
+                    treeOutline.rootDOMNode = domNode;
+
+                    let rootTreeElement = treeOutline.children[0];
+                    if (!rootTreeElement.hasChildren)
+                        treeOutline.element.classList.add("single-node");
+
+                    if (resultData.domAttributes) {
+                        for (let domAttribute of resultData.domAttributes) {
+                            rootTreeElement.highlightAttribute(domAttribute);
+                            rootTreeElement.updateTitle();
+                        }
+                    }
+
+                    dataElement.appendChild(treeOutline.element);
+                } else if (typeof domNode === "string") {
+                    let codeMirror = WI.CodeMirrorEditor.create(dataElement.appendChild(document.createElement("code")), {
+                        mode: "css",
+                        readOnly: true,
+                        lineWrapping: true,
+                        showWhitespaceCharacters: WI.settings.showWhitespaceCharacters.value,
+                        styleSelectedText: true,
+                    });
+                    codeMirror.setValue(domNode);
+
+                    if (resultData.domAttributes) {
+                        for (let domAttribute of resultData.domAttributes) {
+                            let regex = null;
+                            if (domAttribute === "id")
+                                regex = /(\#[^\#|\.|\[|\s|$]+)/g;
+                            else if (domAttribute === "class")
+                                regex = /(\.[^\#|\.|\[|\s|$]+)/g;
+                            else
+                                regex = new RegExp(`\\[\\s*(${domAttribute})\\s*=`, "g");
+
+                            while (true) {
+                                let match = regex.exec(domNode);
+                                if (!match)
+                                    break;
+
+                                let start = match.index + match[0].indexOf(match[1]);
+                                codeMirror.markText({line: 0, ch: start}, {line: 0, ch: start + match[1].length}, {className: "mark"});
+                            }
+                        }
+                    }
+                }
+            });
+        }
+
+        if (resultData.errors && resultData.errors.length) {
+            let errorContainer = this.contentView.element.appendChild(document.createElement("div"));
+            errorContainer.classList.add("errors");
+
+            let errorText = errorContainer.appendChild(document.createElement("h1"));
+            errorText.textContent = WI.UIString("Errors:");
+
+            let tableContainer = errorContainer.appendChild(document.createElement("table"));
+
+            resultData.errors.forEach((error, index) => {
+                let rowElement = tableContainer.appendChild(document.createElement("tr"));
+
+                let indexElement = rowElement.appendChild(document.createElement("td"));
+                indexElement.textContent = index + 1;
+
+                let dataElement = rowElement.appendChild(document.createElement("td"));
+
+                let errorElement = dataElement.appendChild(document.createElement("div"));
+                errorElement.classList.add("error");
+                errorElement.textContent = error;
+            });
+        }
+
+        if (!this.contentView.element.children.length)
+            this.showNoResultDataPlaceholder();
+    }
+
+    showRunningPlaceholder()
+    {
+        if (!this.placeholderElement || !this.placeholderElement.__placeholderRunning) {
+            this.placeholderElement = WI.createMessageTextView(WI.UIString("Running the “%s“ audit").format(this.representedObject.name));
+            this.placeholderElement.__placeholderRunning = true;
+
+            let spinner = new WI.IndeterminateProgressSpinner;
+            this.placeholderElement.appendChild(spinner.element);
+        }
+
+        super.showRunningPlaceholder();
+    }
+};
diff --git a/Source/WebInspectorUI/UserInterface/Views/AuditTestContentView.css b/Source/WebInspectorUI/UserInterface/Views/AuditTestContentView.css
new file mode 100644 (file)
index 0000000..5741112
--- /dev/null
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2018 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+.content-view-container > .content-view.audit-test {
+    overflow-y: scroll;
+}
+
+.content-view-container > .content-view.audit-test > header {
+    padding-top: calc(var(--audit-test-vertical-space) / 5 * 8);
+    padding-bottom: calc(var(--audit-test-vertical-space) / 5 * 8);
+    border-bottom: 1px solid var(--border-color);
+}
+
+.content-view-container > .content-view.audit-test > header h1 {
+    font-size: 2.8em;
+}
+
+.content-view-container > .content-view.audit-test > header p {
+    font-size: 1.25em;
+}
+
+.content-view.audit-test {
+    --audit-test-vertical-space: 10px;
+    --audit-test-horizontal-space: 20px;
+    --audit-test-header-background-color: hsla(0, 0%, 98%, 0.7);
+}
+
+.content-view.audit-test h1 {
+    margin: 0;
+}
+
+.content-view.audit-test > header {
+    padding: var(--audit-test-vertical-space) var(--audit-test-horizontal-space);
+}
+
+.content-view.audit-test > header p {
+    margin: 4px 0 0;
+}
+
+.content-view.audit-test .audit-test.filtered,
+.content-view.audit-test .audit-test .message-text-view {
+    display: none;
+}
+
+.content-view.audit-test > section {
+    position: relative;
+}
+
+.content-view.audit-test > section > .message-text-view {
+    background-color: var(--background-color-content);
+}
+
+.content-view.audit-test.showing-placeholder {
+    display: flex;
+    flex-direction: column;
+}
+
+.content-view.audit-test.showing-placeholder > section {
+    flex-grow: 1;
+}
+
+.content-view.audit-test.showing-placeholder > section > :not(.message-text-view) {
+    display: none;
+}
+
+@media (prefers-dark-interface) {
+    .content-view.audit-test {
+        --audit-test-header-background-color: hsla(0, 0%, 23%, 0.7);
+    }
+}
diff --git a/Source/WebInspectorUI/UserInterface/Views/AuditTestContentView.js b/Source/WebInspectorUI/UserInterface/Views/AuditTestContentView.js
new file mode 100644 (file)
index 0000000..c0c5640
--- /dev/null
@@ -0,0 +1,290 @@
+/*
+ * Copyright (C) 2018 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+WI.AuditTestContentView = class AuditTestContentView extends WI.ContentView
+{
+    constructor(representedObject)
+    {
+        console.assert(representedObject instanceof WI.AuditTestBase || representedObject instanceof WI.AuditTestResultBase);
+
+        super(representedObject);
+
+        this.element.classList.add("audit-test");
+
+        this._exportButtonNavigationItem = new WI.ButtonNavigationItem("audit-export", WI.UIString("Export"), "Images/Export.svg", 15, 15);
+        this._exportButtonNavigationItem.buttonStyle = WI.ButtonNavigationItem.Style.ImageAndText;
+        this._exportButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
+        this._exportButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._handleExportButtonNavigationItemClicked, this);
+        this._updateExportButtonNavigationItemState();
+
+        this._headerView = new WI.View(document.createElement("header"));
+        this._contentView = new WI.View(document.createElement("section"));
+        this._placeholderElement = null;
+
+        this._shownResult = null;
+    }
+
+    // Public
+
+    get navigationItems()
+    {
+        return [this._exportButtonNavigationItem];
+    }
+
+    // Protected
+
+    get headerView() { return this._headerView; }
+    get contentView() { return this._contentView; }
+
+    get supportsSave()
+    {
+        return !!this.representedObject.result;
+    }
+
+    get saveData()
+    {
+        return {customSaveHandler: () => { this._exportAudit(); }};
+    }
+
+    get result()
+    {
+        if (this.representedObject instanceof WI.AuditTestBase)
+            return this.representedObject;
+        return this.representedObject.result;
+    }
+
+    initialLayout()
+    {
+        super.initialLayout();
+
+        this.addSubview(this._headerView);
+        this.addSubview(this._contentView);
+    }
+
+    layout()
+    {
+        super.layout();
+
+        this.hidePlaceholder();
+        this._updateExportButtonNavigationItemState();
+    }
+
+    shown()
+    {
+        super.shown();
+
+        if (this.representedObject instanceof WI.AuditTestBase) {
+            this.representedObject.addEventListener(WI.AuditTestBase.Event.Completed, this._handleTestChanged, this);
+            this.representedObject.addEventListener(WI.AuditTestBase.Event.Progress, this._handleTestChanged, this);
+            this.representedObject.addEventListener(WI.AuditTestBase.Event.ResultCleared, this._handleTestChanged, this);
+            this.representedObject.addEventListener(WI.AuditTestBase.Event.Scheduled, this._handleTestChanged, this);
+            this.representedObject.addEventListener(WI.AuditTestBase.Event.Stopping, this._handleTestChanged, this);
+        }
+    }
+
+    hidden()
+    {
+        if (this.representedObject instanceof WI.AuditTestBase)
+            this.representedObject.removeEventListener(null, null, this);
+
+        super.hidden();
+    }
+
+    get placeholderElement()
+    {
+        return this._placeholderElement;
+    }
+
+    set placeholderElement(placeholderElement)
+    {
+        this.hidePlaceholder();
+
+        this._placeholderElement = placeholderElement;
+    }
+
+    showRunningPlaceholder()
+    {
+        // Overridden by sub-classes.
+
+        console.assert(this.placeholderElement);
+
+        this._showPlaceholder();
+    }
+
+    showStoppingPlaceholder()
+    {
+        if (!this.placeholderElement || !this.placeholderElement.__placeholderStopping) {
+            this.placeholderElement = WI.createMessageTextView(WI.UIString("Stopping the “%s“ audit").format(this.representedObject.name));
+            this.placeholderElement.__placeholderStopping = true;
+
+            let spinner = new WI.IndeterminateProgressSpinner;
+            this.placeholderElement.appendChild(spinner.element);
+        }
+
+        this._showPlaceholder();
+    }
+
+    showNoResultPlaceholder()
+    {
+        if (!this.placeholderElement || !this.placeholderElement.__placeholderNoResult) {
+            this.placeholderElement = WI.createMessageTextView(WI.UIString("No Result"));
+            this.placeholderElement.__placeholderNoResult = true;
+
+            let startNavigationItem = new WI.ButtonNavigationItem("run-audit", WI.UIString("Start"), "Images/AuditStart.svg", 15, 15);
+            startNavigationItem.buttonStyle = WI.ButtonNavigationItem.Style.ImageAndText;
+            startNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, () => {
+                WI.auditManager.start([this.representedObject]);
+            });
+
+            let importHelpElement = WI.createNavigationItemHelp(WI.UIString("Press %s to start running the audit"), startNavigationItem);
+            this.placeholderElement.appendChild(importHelpElement);
+        }
+
+        this._showPlaceholder();
+    }
+
+    showNoResultDataPlaceholder()
+    {
+        if (!this.placeholderElement || !this.placeholderElement.__placeholderNoResultData) {
+            let result = this.representedObject.result;
+            if (!result) {
+                this.showNoResultPlaceholder();
+                return;
+            }
+
+            let message = null;
+            if (result.didError)
+                message = WI.UIString("The “%s“ audit threw an error");
+            else if (result.didFail)
+                message = WI.UIString("The “%s“ audit failed");
+            else if (result.didWarn)
+                message = WI.UIString("The “%s“ audit warned");
+            else if (result.didPass)
+                message = WI.UIString("The “%s“ audit passed");
+            else if (result.unsupported)
+                message = WI.UIString("The “%s“ audit is unsupported");
+            else {
+                console.error("Unknown result", result);
+                return;
+            }
+
+            this.placeholderElement = WI.createMessageTextView(message.format(this.representedObject.name), result.didError);
+            this.placeholderElement.__placeholderNoResultData = true;
+        }
+
+        this._showPlaceholder();
+    }
+
+    showFilteredPlaceholder()
+    {
+        if (!this.placeholderElement || !this.placeholderElement.__placeholderFiltered) {
+            this.placeholderElement = WI.createMessageTextView(WI.UIString("No Filter Results"));
+            this.placeholderElement.__placeholderFiltered = true;
+
+            let buttonElement = this.placeholderElement.appendChild(document.createElement("button"));
+            buttonElement.textContent = WI.UIString("Clear filters");
+            buttonElement.addEventListener("click", () => {
+                this.resetFilter();
+                this.needsLayout();
+            });
+        }
+
+        this._showPlaceholder();
+    }
+
+    hidePlaceholder()
+    {
+        this.element.classList.remove("showing-placeholder");
+        if (this.placeholderElement)
+            this.placeholderElement.remove();
+    }
+
+    applyFilter(levels)
+    {
+        let hasMatch = false;
+        for (let view of this.contentView.subviews) {
+            let matches = view.applyFilter(levels);
+            view.element.classList.toggle("filtered", !matches);
+
+            if (matches)
+                hasMatch = true;
+        }
+
+        this.element.classList.toggle("no-matches", !hasMatch);
+
+        if (!Array.isArray(levels))
+            return true;
+
+        let result = this.representedObject.result;
+        if (!result)
+            return false;
+
+        if ((levels.includes(WI.AuditTestCaseResult.Level.Error) && result.didError)
+            || (levels.includes(WI.AuditTestCaseResult.Level.Fail) && result.didFail)
+            || (levels.includes(WI.AuditTestCaseResult.Level.Warn) && result.didWarn)
+            || (levels.includes(WI.AuditTestCaseResult.Level.Pass) && result.didPass)
+            || (levels.includes(WI.AuditTestCaseResult.Level.Unsupported) && result.unsupported)) {
+            return true;
+        }
+
+        return false;
+    }
+
+    resetFilter()
+    {
+        for (let view of this.contentView.subviews)
+            view.element.classList.remove("filtered");
+
+        this.element.classList.remove("no-matches");
+    }
+
+    // Private
+
+    _exportAudit()
+    {
+        WI.auditManager.export(this.representedObject.result);
+    }
+
+    _updateExportButtonNavigationItemState()
+    {
+        this._exportButtonNavigationItem.enabled = !!this.representedObject.result;
+    }
+
+    _showPlaceholder()
+    {
+        this.element.classList.add("showing-placeholder");
+        this.contentView.element.appendChild(this.placeholderElement);
+    }
+
+    _handleExportButtonNavigationItemClicked(event)
+    {
+        this._exportAudit();
+    }
+
+    _handleTestChanged(event)
+    {
+        this.needsLayout();
+    }
+};
diff --git a/Source/WebInspectorUI/UserInterface/Views/AuditTestGroupContentView.css b/Source/WebInspectorUI/UserInterface/Views/AuditTestGroupContentView.css
new file mode 100644 (file)
index 0000000..000746c
--- /dev/null
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2018 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+.content-view-container > .content-view.audit-test-group > header {
+    background-color: var(--background-color-content);
+}
+
+.content-view.audit-test-group > header {
+    display: flex;
+    align-items: center;
+    margin-top: -1px;
+    border-top: 1px solid var(--border-color);
+}
+
+.content-view.audit-test-group.no-matches + .audit-test-group > header {
+    border-top: none;
+}
+
+.content-view.audit-test-group > header,
+.content-view.audit-test-group:not(.filtered):last-child > header {
+    border-bottom: 1px solid var(--border-color);
+}
+
+.content-view.audit-test-group.contains-test-case > header {
+    position: -webkit-sticky;
+    top: -1px;
+    z-index: var(--z-index-header);
+    background-color: var(--audit-test-header-background-color);
+    -webkit-backdrop-filter: blur(20px);
+}
+
+.content-view.audit-test-group.contains-test-case + .audit-test-group.contains-test-case {
+    border-top: 1px solid var(--border-color);
+}
+
+.content-view.audit-test-group.contains-test-case:not(.contains-test-group) > section,
+.content-view.audit-test-group.contains-test-case.contains-test-group > section > .audit-test-case {
+    padding-right: calc(var(--audit-test-horizontal-space) / 2);
+    padding-left: calc(var(--audit-test-horizontal-space) / 2);
+}
+
+.content-view.audit-test-group > header > .information {
+    flex-grow: 1;
+}
+
+.content-view.audit-test-group > header > .information > p {
+    font-size: 1.25em;
+}
+
+.content-view.audit-test-group > header > nav {
+    display: inline-flex;
+    height: auto;
+    border-bottom: none;
+}
+
+.content-view.audit-test-group > header > nav:empty {
+    display: none;
+}
+
+.content-view.audit-test-group > header > nav:not(:empty):before {
+    content: attr(data-prefix);
+}
+
+.content-view.audit-test-group > header > nav > .scope-bar > li {
+    display: inline-flex;
+    align-items: center;
+    margin: 0 3px;
+}
+
+.content-view.audit-test-group > header > nav > .scope-bar > li:not(:hover, .selected) {
+    border: 1px solid var(--selected-background-color-unfocused);
+}
+
+.content-view.audit-test-group > header > nav > .scope-bar > li:last-child {
+    -webkit-margin-end: 0;
+}
+
+.content-view.audit-test-group > header > nav > .scope-bar > li::before {
+    width: 16px;
+    height: 16px;
+    margin-top: 1px;
+    -webkit-margin-end: 4px;
+}
+
+.content-view.audit-test-group > header > nav > .scope-bar > li.pass::before {
+    content: url(../Images/AuditTestPass.svg);
+}
+
+.content-view.audit-test-group > header > nav > .scope-bar > li.warn::before {
+    content: url(../Images/AuditTestWarn.svg);
+}
+
+.content-view.audit-test-group > header > nav > .scope-bar > li.fail::before {
+    content: url(../Images/AuditTestFail.svg);
+}
+
+.content-view.audit-test-group > header > nav > .scope-bar > li.error::before {
+    content: url(../Images/AuditTestError.svg);
+}
+
+.content-view.audit-test-group > header > nav > .scope-bar > li.unsupported::before {
+    content: url(../Images/AuditTestUnsupported.svg);
+}
+
+.content-view.audit-test-group > header > .percentage-pass {
+    -webkit-margin-start: var(--audit-test-horizontal-space);
+    width: 60px;
+    font-size: 24px;
+    text-align: center;
+    font-weight: bold;
+    opacity: 0.65;
+}
+
+.content-view.audit-test-group > header > .percentage-pass:not(:empty)::after {
+    content: "%";
+    font-size: 16px;
+    opacity: 0.75;
+}
+
+.content-view.audit-test-group > section > .audit-test-case:first-child,
+.content-view.audit-test-group > section > .audit-test-group + .audit-test-case,
+.content-view.audit-test-group > section > .audit-test-case + .audit-test-group {
+    margin-top: var(--audit-test-vertical-space);
+}
+
+.content-view.audit-test-group > section > .audit-test-case:last-child {
+    margin-bottom: var(--audit-test-vertical-space);
+}
diff --git a/Source/WebInspectorUI/UserInterface/Views/AuditTestGroupContentView.js b/Source/WebInspectorUI/UserInterface/Views/AuditTestGroupContentView.js
new file mode 100644 (file)
index 0000000..04bf533
--- /dev/null
@@ -0,0 +1,233 @@
+/*
+ * Copyright (C) 2018 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+WI.AuditTestGroupContentView = class AuditTestGroupContentView extends WI.AuditTestContentView
+{
+    constructor(representedObject)
+    {
+        console.assert(representedObject instanceof WI.AuditTestGroup || representedObject instanceof WI.AuditTestGroupResult);
+
+        super(representedObject);
+
+        this.element.classList.add("audit-test-group");
+        this.element.classList.toggle("contains-test-case", this._subobjects().some((test) => test instanceof WI.AuditTestCase || test instanceof WI.AuditTestCaseResult));
+        this.element.classList.toggle("contains-test-group", this._subobjects().some((test) => test instanceof WI.AuditTestGroup || test instanceof WI.AuditTestGroupResult));
+
+        this._levelScopeBar = null;
+    }
+
+    // Protected
+
+    initialLayout()
+    {
+        super.initialLayout();
+
+        let informationContainer = this.headerView.element.appendChild(document.createElement("div"));
+        informationContainer.classList.add("information");
+
+        let nameElement = informationContainer.appendChild(document.createElement("h1"));
+        nameElement.textContent = this.representedObject.name;
+
+        if (this.representedObject.description) {
+            let descriptionElement = informationContainer.appendChild(document.createElement("p"));
+            descriptionElement.textContent = this.representedObject.description;
+        }
+
+        this._levelNavigationBar = new WI.NavigationBar(document.createElement("nav"));
+        this._levelNavigationBar.element.dataset.prefix = WI.UIString("Showing:");
+        this.headerView.addSubview(this._levelNavigationBar);
+
+        this._percentageTextElement = this.headerView.element.appendChild(document.createElement("div"));
+        this._percentageTextElement.classList.add("percentage-pass");
+        this.headerView.element.appendChild(this._percentageTextElement);
+    }
+
+    layout()
+    {
+        if (this.layoutReason !== WI.View.LayoutReason.Dirty)
+            return;
+
+        super.layout();
+
+        let result = this.representedObject.result;
+        if (!result) {
+            if (this._levelScopeBar) {
+                this._levelNavigationBar.removeNavigationItem(this._levelScopeBar);
+                this._levelScopeBar = null;
+            }
+
+            this._percentageTextElement.textContent = "";
+
+            if (this.representedObject.runningState === WI.AuditManager.RunningState.Inactive)
+                this.showNoResultPlaceholder();
+            else if (this.representedObject.runningState === WI.AuditManager.RunningState.Active)
+                this.showRunningPlaceholder();
+            else if (this.representedObject.runningState === WI.AuditManager.RunningState.Stopping)
+                this.showStoppingPlaceholder();
+
+            return;
+        }
+
+        let levelCounts = result.levelCounts;
+        let totalCount = Object.values(levelCounts).reduce((accumulator, current) => accumulator + current);
+        this._percentageTextElement.textContent = Math.floor(100 * levelCounts[WI.AuditTestCaseResult.Level.Pass] / totalCount);
+
+        if (!this._levelScopeBar) {
+            let scopeBarItems = [];
+
+            let addScopeBarItem = (level, label) => {
+                let count = levelCounts[level];
+                if (isNaN(count) || count <= 0)
+                    return;
+
+                let scopeBarItem = new WI.ScopeBarItem(level, label.format(count), {
+                    className: level,
+                    exclusive: false,
+                    independent: true,
+                });
+                scopeBarItem.selected = true;
+                scopeBarItems.push(scopeBarItem);
+            };
+
+            addScopeBarItem(WI.AuditTestCaseResult.Level.Pass, WI.UIString("%d Pass"));
+            addScopeBarItem(WI.AuditTestCaseResult.Level.Warn, WI.UIString("%d Warn"));
+            addScopeBarItem(WI.AuditTestCaseResult.Level.Fail, WI.UIString("%d Fail"));
+            addScopeBarItem(WI.AuditTestCaseResult.Level.Error, WI.UIString("%d Error"));
+            addScopeBarItem(WI.AuditTestCaseResult.Level.Unsupported, WI.UIString("%d Unsupported"));
+
+            this._levelScopeBar = new WI.ScopeBar(null, scopeBarItems);
+            this._levelScopeBar.addEventListener(WI.ScopeBar.Event.SelectionChanged, this._handleLevelScopeBarSelectionChanged, this);
+            this._levelNavigationBar.addNavigationItem(this._levelScopeBar);
+        }
+
+        if (this.applyFilter())
+            this.hidePlaceholder();
+        else
+            this.showFilteredPlaceholder();
+    }
+
+    shown()
+    {
+        super.shown();
+
+        if (this.representedObject instanceof WI.AuditTestGroup) {
+            this.representedObject.addEventListener(WI.AuditTestBase.Event.Progress, this._handleTestGroupProgress, this);
+            this.representedObject.addEventListener(WI.AuditTestBase.Event.Scheduled, this._handleTestGroupScheduled, this);
+        }
+
+        for (let subobject of this._subobjects()) {
+            let view = WI.ContentView.contentViewForRepresentedObject(subobject);
+            this.contentView.addSubview(view);
+            view.shown();
+        }
+    }
+
+    hidden()
+    {
+        for (let view of this.contentView.subviews)
+            view.hidden();
+
+        this.contentView.removeAllSubviews();
+
+        super.hidden();
+    }
+
+    applyFilter(levels)
+    {
+        if (this._levelScopeBar && !levels)
+            levels = this._levelScopeBar.selectedItems.map((item) => item.id);
+
+        this._updateLevelScopeBar(levels);
+
+        return super.applyFilter(levels);
+    }
+
+    resetFilter()
+    {
+        this._updateLevelScopeBar(Object.values(WI.AuditTestCaseResult.Level));
+
+        super.resetFilter();
+    }
+
+    showRunningPlaceholder()
+    {
+        if (!this.placeholderElement || !this.placeholderElement.__placeholderRunning) {
+            this.placeholderElement = WI.createMessageTextView(WI.UIString("Running the “%s“ audit").format(this.representedObject.name));
+            this.placeholderElement.__placeholderRunning = true;
+
+            this.placeholderElement.__progress = document.createElement("progress");
+            this.placeholderElement.__progress.value = 0;
+            this.placeholderElement.appendChild(this.placeholderElement.__progress);
+        }
+
+        super.showRunningPlaceholder();
+    }
+
+    // Private
+
+    _subobjects()
+    {
+        if (this.representedObject instanceof WI.AuditTestGroup)
+            return this.representedObject.tests;
+
+        if (this.representedObject instanceof WI.AuditTestGroupResult)
+            return this.representedObject.results;
+
+        console.error("Unknown representedObject", this.representedObject);
+        return [];
+    }
+
+    _updateLevelScopeBar(levels)
+    {
+        if (!this._levelScopeBar)
+            return;
+
+        for (let item of this._levelScopeBar.items)
+            item.selected = levels.includes(item.id);
+
+        for (let view of this.contentView.subviews) {
+            if (view instanceof WI.AuditTestGroupContentView)
+                view._updateLevelScopeBar(levels);
+        }
+    }
+
+    _handleTestGroupProgress(event)
+    {
+        let {index, count} = event.data;
+        if (this.placeholderElement && this.placeholderElement.__progress)
+            this.placeholderElement.__progress.value = (index + 1) / count;
+    }
+
+    _handleTestGroupScheduled(event)
+    {
+        if (this.placeholderElement && this.placeholderElement.__progress)
+            this.placeholderElement.__progress.value = 0;
+    }
+
+    _handleLevelScopeBarSelectionChanged(event)
+    {
+        this.needsLayout();
+    }
+};
diff --git a/Source/WebInspectorUI/UserInterface/Views/AuditTreeElement.css b/Source/WebInspectorUI/UserInterface/Views/AuditTreeElement.css
new file mode 100644 (file)
index 0000000..e1f8be3
--- /dev/null
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2018 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+.tree-outline .item.audit > .status {
+    display: flex;
+    align-items: center;
+    width: 16px;
+    height: 16px;
+}
+
+.tree-outline .item.audit > .status > img {
+    width: 100%;
+    height: 100%;
+}
+
+.tree-outline .item.audit:matches(.test-case, .test-group) > .status:hover > img {
+    width: 75%;
+    height: 75%;
+    margin: 0 auto;
+    content: url(../Images/AuditStart.svg) !important;
+}
+
+.tree-outline .item.audit > .status:not(:hover) > img.show-on-hover,
+.tree-outline .item.audit.test-group.expanded > .status:not(:hover) {
+    opacity: 0;
+}
+
+.tree-outline .item.audit.test-group.expanded > .status:hover > :not(img),
+.tree-outline .item.audit.test-group-result.expanded > .status {
+    display: none;
+}
+
+.tree-outline .item.audit > .status > img.pass {
+    content: url(../Images/AuditTestPass.svg);
+}
+
+.tree-outline .item.audit > .status > img.warn {
+    content: url(../Images/AuditTestWarn.svg);
+}
+
+.tree-outline .item.audit > .status > img.fail {
+    content: url(../Images/AuditTestFail.svg);
+}
+
+.tree-outline .item.audit > .status > img.error {
+    content: url(../Images/AuditTestError.svg);
+}
+
+.tree-outline .item.audit > .status > img.unsupported {
+    content: url(../Images/AuditTestUnsupported.svg);
+}
+
+.audit.test-case .icon {
+    content: url(../Images/AuditTestCase.svg);
+}
+
+.audit.test-group .icon {
+    content: url(../Images/AuditTestGroup.svg);
+}
+
+.audit.test-case-result .icon {
+    content: url(../Images/AuditTestCaseResult.svg);
+}
+
+.audit.test-group-result .icon {
+    content: url(../Images/AuditTestGroupResult.svg);
+}
diff --git a/Source/WebInspectorUI/UserInterface/Views/AuditTreeElement.js b/Source/WebInspectorUI/UserInterface/Views/AuditTreeElement.js
new file mode 100644 (file)
index 0000000..f685f6e
--- /dev/null
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2018 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+WI.AuditTreeElement = class AuditTreeElement extends WI.GeneralTreeElement
+{
+    constructor(representedObject)
+    {
+        let isTestCase = representedObject instanceof WI.AuditTestCase;
+        let isTestGroup = representedObject instanceof WI.AuditTestGroup;
+        let isTestCaseResult = representedObject instanceof WI.AuditTestCaseResult;
+        let isTestGroupResult = representedObject instanceof WI.AuditTestGroupResult;
+        console.assert(isTestCase || isTestGroup || isTestCaseResult || isTestGroupResult);
+
+        let classNames = ["audit"];
+        if (isTestCase)
+            classNames.push("test-case");
+        else if (isTestGroup)
+            classNames.push("test-group");
+        else if (isTestCaseResult)
+            classNames.push("test-case-result");
+        else if (isTestGroupResult)
+            classNames.push("test-group-result");
+
+        let options = {
+            hasChildren: isTestGroup || isTestGroupResult,
+        };
+
+        const subtitle = null;
+        super(classNames, representedObject.name, subtitle, representedObject, options);
+    }
+
+    // Protected
+
+    onattach()
+    {
+        super.onattach();
+
+        if (this.representedObject instanceof WI.AuditTestBase) {
+            this.representedObject.addEventListener(WI.AuditTestBase.Event.ResultCleared, this._handleTestResultCleared, this);
+
+            if (this.representedObject instanceof WI.AuditTestCase)
+                this.representedObject.addEventListener(WI.AuditTestBase.Event.Scheduled, this._handleTestCaseScheduled, this);
+            else if (this.representedObject instanceof WI.AuditTestGroup) {
+                this.representedObject.addEventListener(WI.AuditTestBase.Event.Scheduled, this._handleTestGroupScheduled, this);
+                this.expand();
+            }
+        }
+
+        this._updateLevel();
+    }
+
+    ondetach()
+    {
+        this.representedObject.removeEventListener(null, null, this);
+
+        super.ondetach();
+    }
+
+    onpopulate()
+    {
+        super.onpopulate();
+
+        if (this.children.length && !this.shouldRefreshChildren)
+            return;
+
+        this.shouldRefreshChildren = false;
+
+        this.removeChildren();
+
+        if (this.representedObject instanceof WI.AuditTestGroup) {
+            for (let test of this.representedObject.tests)
+                this.appendChild(new WI.AuditTreeElement(test));
+        } else if (this.representedObject instanceof WI.AuditTestGroupResult) {
+            for (let result of this.representedObject.results)
+                this.appendChild(new WI.AuditTreeElement(result));
+        }
+    }
+
+    populateContextMenu(contextMenu, event)
+    {
+        if (WI.auditManager.runningState === WI.AuditManager.RunningState.Inactive) {
+            contextMenu.appendItem(WI.UIString("Start"), (event) => {
+                this._start();
+            });
+        }
+
+        contextMenu.appendSeparator();
+
+        if (this.representedObject instanceof WI.AuditTestCase || this.representedObject instanceof WI.AuditTestGroup) {
+            contextMenu.appendItem(WI.UIString("Export Test"), (event) => {
+                WI.auditManager.export(this.representedObject);
+            });
+        }
+
+        if (this.representedObject.result) {
+            contextMenu.appendItem(WI.UIString("Export Result"), (event) => {
+                WI.auditManager.export(this.representedObject.result);
+            });
+        }
+
+        contextMenu.appendSeparator();
+
+        super.populateContextMenu(contextMenu, event);
+    }
+
+    // Private
+
+    _start()
+    {
+        if (WI.auditManager.runningState !== WI.AuditManager.RunningState.Inactive)
+            return;
+
+        WI.auditManager.start([this.representedObject]);
+    }
+
+    _updateLevel()
+    {
+        let className = "show-on-hover";
+
+        let result = this.representedObject.result;
+        if (result) {
+            if (result.didError)
+                className = WI.AuditTestCaseResult.Level.Error;
+            else if (result.didFail)
+                className = WI.AuditTestCaseResult.Level.Fail;
+            else if (result.didWarn)
+                className = WI.AuditTestCaseResult.Level.Warn;
+            else if (result.didPass)
+                className = WI.AuditTestCaseResult.Level.Pass;
+            else if (result.unsupported)
+                className = WI.AuditTestCaseResult.Level.Unsupported;
+        }
+
+        this.status = document.createElement("img");
+        this.status.classList.add(className);
+
+        if (this.representedObject instanceof WI.AuditTestCase || this.representedObject instanceof WI.AuditTestGroup) {
+            this.status.title = WI.UIString("Start");
+            this.status.addEventListener("click", this._handleStatusClick.bind(this));
+        }
+    }
+
+    _showRunningSpinner()
+    {
+        if (this.representedObject.runningState === WI.AuditManager.RunningState.Inactive) {
+            this._updateLevel();
+            return;
+        }
+
+        if (!this.status || !this.status.__spinner) {
+            let spinner = new WI.IndeterminateProgressSpinner;
+            this.status = spinner.element;
+            this.status.__spinner = true;
+        }
+    }
+
+    _showRunningProgress(progress)
+    {
+        if (!this.representedObject.runningState === WI.AuditManager.RunningState.Inactive) {
+            this._updateLevel();
+            return;
+        }
+
+        if (!this.status || !this.status.__progress) {
+            this.status = document.createElement("progress");
+            this.status.__progress = true;
+        }
+
+        this.status.value = progress || 0;
+    }
+
+    _handleTestCaseCompleted(event)
+    {
+        this.representedObject.removeEventListener(WI.AuditTestBase.Event.Completed, this._handleTestCaseCompleted, this);
+
+        this._updateLevel();
+    }
+
+    _handleTestResultCleared(event)
+    {
+        this._updateLevel();
+    }
+
+    _handleTestCaseScheduled(event)
+    {
+        this.representedObject.addEventListener(WI.AuditTestBase.Event.Completed, this._handleTestCaseCompleted, this);
+
+        this._showRunningSpinner();
+    }
+
+    _handleTestGroupCompleted(event)
+    {
+        this.representedObject.removeEventListener(WI.AuditTestBase.Event.Completed, this._handleTestGroupCompleted, this);
+        this.representedObject.removeEventListener(WI.AuditTestBase.Event.Progress, this._handleTestGroupProgress, this);
+
+        this._updateLevel();
+    }
+
+    _handleTestGroupProgress(event)
+    {
+        let {index, count} = event.data;
+        this._showRunningProgress((index + 1) / count);
+    }
+
+    _handleTestGroupScheduled(event)
+    {
+        this.representedObject.addEventListener(WI.AuditTestBase.Event.Completed, this._handleTestGroupCompleted, this);
+        this.representedObject.addEventListener(WI.AuditTestBase.Event.Progress, this._handleTestGroupProgress, this);
+
+        this._showRunningProgress();
+    }
+
+    _handleStatusClick(event)
+    {
+        this._start();
+    }
+};
index da90559..1a11e7f 100644 (file)
@@ -167,6 +167,12 @@ WI.ContentView = class ContentView extends WI.View
         if (representedObject instanceof WI.ResourceCollection)
             return new WI.ResourceCollectionContentView(representedObject, extraArguments);
 
+        if (representedObject instanceof WI.AuditTestCase || representedObject instanceof WI.AuditTestCaseResult)
+            return new WI.AuditTestCaseContentView(representedObject, extraArguments);
+
+        if (representedObject instanceof WI.AuditTestGroup || representedObject instanceof WI.AuditTestGroupResult)
+            return new WI.AuditTestGroupContentView(representedObject, extraArguments);
+
         if (representedObject instanceof WI.Collection)
             return new WI.CollectionContentView(representedObject, extraArguments);
 
@@ -295,6 +301,9 @@ WI.ContentView = class ContentView extends WI.View
             return true;
         if (representedObject instanceof WI.Recording)
             return true;
+        if (representedObject instanceof WI.AuditTestCase || representedObject instanceof WI.AuditTestGroup
+            || representedObject instanceof WI.AuditTestCaseResult || representedObject instanceof WI.AuditTestGroupResult)
+            return true;
         if (representedObject instanceof WI.Collection)
             return true;
         if (typeof representedObject === "string" || representedObject instanceof String)
index 3109576..75e7721 100644 (file)
@@ -47,6 +47,7 @@ WI.DOMTreeElement = class DOMTreeElement extends WI.TreeElement
         this._boundHighlightAnimationEnd = this._highlightAnimationEnd.bind(this);
         this._subtreeBreakpointCount = 0;
 
+        this._highlightedAttributes = new Set;
         this._recentlyModifiedAttributes = [];
         this._boundNodeChangedAnimationEnd = this._nodeChangedAnimationEnd.bind(this);
 
@@ -268,6 +269,11 @@ WI.DOMTreeElement = class DOMTreeElement extends WI.TreeElement
         this._recentlyModifiedAttributes.push({name});
     }
 
+    highlightAttribute(name)
+    {
+        this._highlightedAttributes.add(name);
+    }
+
     showChildNode(node)
     {
         console.assert(!this._elementCloseTag);
@@ -1327,6 +1333,9 @@ WI.DOMTreeElement = class DOMTreeElement extends WI.TreeElement
             if (attribute.name === name)
                 attribute.element = hasText ? attrValueElement : attrNameElement;
         }
+
+        if (this._highlightedAttributes.has(name))
+            attrSpanElement.classList.add("highlight");
     }
 
     _buildTagDOM(parentElement, tagName, isClosingTag, isDistinctTreeElement)
index 59b2a4f..2b2887a 100644 (file)
@@ -307,6 +307,11 @@ body[dir=rtl] .tree-outline.dom li.parent.shadow::after {
     animation: "dom-tree-outline-highlight-fadeout" 2s;
 }
 
+.tree-outline.dom li .highlight {
+    background-color: hsla(53, 83%, 53%, 0.2);
+    border-bottom: 1px solid hsl(47, 82%, 60%);
+}
+
 @media (prefers-dark-interface) {
     .tree-outline.dom li.elements-drag-over .selection-area {
         border-top-color: var(--selected-background-color);
index 7b3d5df..778d5ee 100644 (file)
@@ -25,7 +25,7 @@
 
 .navigation-bar .item.divider {
     width: 1px;
-
+    height: 100%;
     background-image: linear-gradient(hsl(0, 0%, 74%), hsl(0, 0%, 74%));
     background-size: 100% 16px;
     background-repeat: no-repeat;
index 7378ea4..427708d 100644 (file)
@@ -199,7 +199,7 @@ body.docked:matches(.right, .left) #navigation-sidebar.collapsed > .resizer {
     height: 20px;
     padding: 0 4px;
     border-bottom: none;
-    vertical-align: middle;
+    vertical-align: -3px;
 }
 
 .message-text-view .navigation-item-help .navigation-bar > .item {
index 47b9169..64ceb9d 100644 (file)
@@ -25,7 +25,7 @@
 
 WI.ScopeBarItem = class ScopeBarItem extends WI.Object
 {
-    constructor(id, label, {className, exclusive, hidden} = {})
+    constructor(id, label, {className, exclusive, independent, hidden} = {})
     {
         super();
 
@@ -39,6 +39,7 @@ WI.ScopeBarItem = class ScopeBarItem extends WI.Object
         this._id = id;
         this._label = label;
         this._exclusive = !!exclusive;
+        this._independent = !!independent;
         this._hidden = !!hidden;
 
         this._selectedSetting = new WI.Setting("scopebaritem-" + id, false);
@@ -83,7 +84,7 @@ WI.ScopeBarItem = class ScopeBarItem extends WI.Object
         this._selectedSetting.value = selected;
 
         this.dispatchEventToListeners(WI.ScopeBarItem.Event.SelectionChanged, {
-            extendSelection: WI.modifierKeys.metaKey && !WI.modifierKeys.ctrlKey && !WI.modifierKeys.altKey && !WI.modifierKeys.shiftKey,
+            extendSelection: this._independent || (WI.modifierKeys.metaKey && !WI.modifierKeys.ctrlKey && !WI.modifierKeys.altKey && !WI.modifierKeys.shiftKey),
         });
     }
 
index 1904682..54b2a9e 100644 (file)
@@ -249,6 +249,9 @@ WI.SettingsTabContentView = class SettingsTabContentView extends WI.TabContentVi
             experimentalSettingsView.addSeparator();
         }
 
+        experimentalSettingsView.addSetting(WI.UIString("Audit:"), WI.settings.experimentalEnableAuditTab, WI.UIString("Enable Audit Tab"));
+        experimentalSettingsView.addSeparator();
+
         experimentalSettingsView.addSetting(WI.UIString("User Interface:"), WI.settings.experimentalEnableNewTabBar, WI.UIString("Enable New Tab Bar"));
         experimentalSettingsView.addSeparator();
 
@@ -268,6 +271,7 @@ WI.SettingsTabContentView = class SettingsTabContentView extends WI.TabContentVi
 
         listenForChange(WI.settings.experimentalEnableMultiplePropertiesSelection);
         listenForChange(WI.settings.experimentalEnableLayersTab);
+        listenForChange(WI.settings.experimentalEnableAuditTab);
         listenForChange(WI.settings.experimentalEnableNewTabBar);
 
         this.addSettingsView(experimentalSettingsView);
index 72a5838..97c1048 100644 (file)
@@ -103,6 +103,9 @@ WI.ToggleButtonNavigationItem = class ToggleButtonNavigationItem extends WI.Butt
             this.tooltip = this._defaultToolTip;
             this.image = this._defaultImage;
         }
+
+        if (this.buttonStyle === WI.ButtonNavigationItem.Style.Text || this.buttonStyle === WI.ButtonNavigationItem.Style.ImageAndText)
+            this.label = this.tooltip;
     }
 
     // Protected