Web Inspector: Sources: provide a way to create an arbitrary Inspector Style Sheet
authordrousso@apple.com <drousso@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 16 Aug 2019 00:43:11 +0000 (00:43 +0000)
committerdrousso@apple.com <drousso@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 16 Aug 2019 00:43:11 +0000 (00:43 +0000)
https://bugs.webkit.org/show_bug.cgi?id=200425

Reviewed by Joseph Pecoraro.

Source/WebInspectorUI:

Right now, the only way to create an Inspector Style Sheet is by creating a new rule in the
Styles sidebar of the Elements Tab. This is unnecessarily restrictive, especially for those
who don't use the Elements tab.

Add a + button after the filter bar in the Navigation sidebar. Clicking on the + button will
show a menu with the following (more likely to be added later):
 - Inspector Style Sheet
 - Frames (if there are subframes)
   - (name of subframe)
      - Inspector Style Sheet

* UserInterface/Views/SourcesNavigationSidebarPanel.js:
(WI.SourcesNavigationSidebarPanel):
(WI.SourcesNavigationSidebarPanel.prototype.treeElementForRepresentedObject): Added.
(WI.SourcesNavigationSidebarPanel.prototype._filterByResourcesWithIssues): Added.
(WI.SourcesNavigationSidebarPanel.prototype._compareTreeElements):
(WI.SourcesNavigationSidebarPanel.prototype._updateMainFrameTreeElement):
(WI.SourcesNavigationSidebarPanel.prototype._addResource):
(WI.SourcesNavigationSidebarPanel.prototype._handleTreeSelectionDidChange):
(WI.SourcesNavigationSidebarPanel.prototype._populateCreateResourceContextMenu): Added.
(WI.SourcesNavigationSidebarPanel.prototype._handleResourceGroupingModeChanged):
(WI.SourcesNavigationSidebarPanel.prototype._handleFrameWasAdded): Added.
(WI.SourcesNavigationSidebarPanel.prototype._handleMainFrameDidChange): Deleted.
* UserInterface/Views/GeneralTreeElement.js:
(WI.GeneralTreeElement.prototype.createFoldersAsNeededForSubpath):
Drive-by: sort `WI.ResourceTreeElement`s alongside `WI.FolderTreeElement`s for easier readability.
* UserInterface/Views/FrameTreeElement.js:
(WI.FrameTreeElement.prototype.onpopulate):
Add all `inspectorStyleSheetsForFrame` instead of just the preferred one so that they all
are visible/selectable for editing.

* UserInterface/Views/FilterBar.js:
(WI.FilterBar):
* UserInterface/Views/FilterBar.css:
(.filter-bar > .navigation-bar > .item):
(.filter-bar > input[type="search"]):
(.filter-bar > .navigation-bar + input[type="search"]): Added.
(.filter-bar > input[type="search"] + .navigation-bar:empty): Added.
Move the position of the filter bar buttons to be after the filter bar itself, so that other
parents can add action items before the filter bar to keep a consistent positioning.
 - to the left of the filter bar are action items (e.g. "+")
 - the filter bar itself
 - to the right of the filter bar are filter buttons (e.g. "filter by resoure with issue")

* UserInterface/Controllers/NetworkManager.js:
(WI.NetworkManager.prototype.get frames):
Drive-by: use `Array.from`, instead of `[...map.values()]`.
* UserInterface/Models/Frame.js:
(WI.Frame.prototype.get url):
(WI.Frame.prototype.get urlComponents): Added.

* UserInterface/Base/URLUtilities.js.js:
(parseURL):
Calculate and include the `origin` string with the output.

* UserInterface/Controllers/CSSManager.js:
(WI.CSSManager.prototype.preferredInspectorStyleSheetForFrame):
Remove `doNotCreateIfMissing` now that the last caller has been removed.

* Localizations/en.lproj/localizedStrings.js:

LayoutTests:

* inspector/unit-tests/url-utilities.html:
* inspector/unit-tests/url-utilities-expected.txt:

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

14 files changed:
LayoutTests/ChangeLog
LayoutTests/inspector/unit-tests/url-utilities-expected.txt
LayoutTests/inspector/unit-tests/url-utilities.html
Source/WebInspectorUI/ChangeLog
Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js
Source/WebInspectorUI/UserInterface/Base/URLUtilities.js
Source/WebInspectorUI/UserInterface/Controllers/CSSManager.js
Source/WebInspectorUI/UserInterface/Controllers/NetworkManager.js
Source/WebInspectorUI/UserInterface/Models/Frame.js
Source/WebInspectorUI/UserInterface/Views/FilterBar.css
Source/WebInspectorUI/UserInterface/Views/FilterBar.js
Source/WebInspectorUI/UserInterface/Views/FrameTreeElement.js
Source/WebInspectorUI/UserInterface/Views/GeneralTreeElement.js
Source/WebInspectorUI/UserInterface/Views/SourcesNavigationSidebarPanel.js

index 379ebbc..be46711 100644 (file)
@@ -1,3 +1,13 @@
+2019-08-15  Devin Rousso  <drousso@apple.com>
+
+        Web Inspector: Sources: provide a way to create an arbitrary Inspector Style Sheet
+        https://bugs.webkit.org/show_bug.cgi?id=200425
+
+        Reviewed by Joseph Pecoraro.
+
+        * inspector/unit-tests/url-utilities.html:
+        * inspector/unit-tests/url-utilities-expected.txt:
+
 2019-08-15  Wenson Hsieh  <wenson_hsieh@apple.com>
 
         Tidy up some event stream helpers in basic-gestures.js
index e50ca8f..56fd016 100644 (file)
@@ -15,6 +15,7 @@ PASS: scheme should be: 'http'
 PASS: userinfo should be: 'null'
 PASS: host should be: 'example.com'
 PASS: port should be: 'null'
+PASS: origin should be: 'http://example.com'
 PASS: path should be: 'null'
 PASS: queryString should be: 'null'
 PASS: fragment should be: 'null'
@@ -25,6 +26,7 @@ PASS: scheme should be: 'http'
 PASS: userinfo should be: 'null'
 PASS: host should be: 'example.com'
 PASS: port should be: 'null'
+PASS: origin should be: 'http://example.com'
 PASS: path should be: '/'
 PASS: queryString should be: 'null'
 PASS: fragment should be: 'null'
@@ -35,6 +37,7 @@ PASS: scheme should be: 'http'
 PASS: userinfo should be: 'null'
 PASS: host should be: 'example.com'
 PASS: port should be: '80'
+PASS: origin should be: 'http://example.com:80'
 PASS: path should be: '/'
 PASS: queryString should be: 'null'
 PASS: fragment should be: 'null'
@@ -45,6 +48,7 @@ PASS: scheme should be: 'http'
 PASS: userinfo should be: 'null'
 PASS: host should be: 'example.com'
 PASS: port should be: 'null'
+PASS: origin should be: 'http://example.com'
 PASS: path should be: '/path/to/page.html'
 PASS: queryString should be: 'null'
 PASS: fragment should be: 'null'
@@ -55,6 +59,7 @@ PASS: scheme should be: 'http'
 PASS: userinfo should be: 'null'
 PASS: host should be: 'example.com'
 PASS: port should be: 'null'
+PASS: origin should be: 'http://example.com'
 PASS: path should be: '/path/to/page.html'
 PASS: queryString should be: ''
 PASS: fragment should be: 'null'
@@ -65,6 +70,7 @@ PASS: scheme should be: 'http'
 PASS: userinfo should be: 'null'
 PASS: host should be: 'example.com'
 PASS: port should be: 'null'
+PASS: origin should be: 'http://example.com'
 PASS: path should be: '/path/to/page.html'
 PASS: queryString should be: 'a=1'
 PASS: fragment should be: 'null'
@@ -75,6 +81,7 @@ PASS: scheme should be: 'http'
 PASS: userinfo should be: 'null'
 PASS: host should be: 'example.com'
 PASS: port should be: 'null'
+PASS: origin should be: 'http://example.com'
 PASS: path should be: '/path/to/page.html'
 PASS: queryString should be: 'a=1&b=2'
 PASS: fragment should be: 'null'
@@ -85,6 +92,7 @@ PASS: scheme should be: 'http'
 PASS: userinfo should be: 'null'
 PASS: host should be: 'example.com'
 PASS: port should be: 'null'
+PASS: origin should be: 'http://example.com'
 PASS: path should be: '/path/to/page.html'
 PASS: queryString should be: 'a=1&b=2'
 PASS: fragment should be: 'test'
@@ -95,6 +103,7 @@ PASS: scheme should be: 'http'
 PASS: userinfo should be: 'null'
 PASS: host should be: 'example.com'
 PASS: port should be: '123'
+PASS: origin should be: 'http://example.com:123'
 PASS: path should be: '/path/to/page.html'
 PASS: queryString should be: 'a=1&b=2'
 PASS: fragment should be: 'test'
@@ -105,6 +114,7 @@ PASS: scheme should be: 'http'
 PASS: userinfo should be: 'null'
 PASS: host should be: 'example.com'
 PASS: port should be: 'null'
+PASS: origin should be: 'http://example.com'
 PASS: path should be: '/path/to/page.html'
 PASS: queryString should be: 'null'
 PASS: fragment should be: 'test'
@@ -115,6 +125,7 @@ PASS: scheme should be: 'http'
 PASS: userinfo should be: 'null'
 PASS: host should be: 'example.com'
 PASS: port should be: 'null'
+PASS: origin should be: 'http://example.com'
 PASS: path should be: 'null'
 PASS: queryString should be: 'null'
 PASS: fragment should be: 'alpha/beta'
@@ -125,6 +136,7 @@ PASS: scheme should be: 'app-specific'
 PASS: userinfo should be: 'null'
 PASS: host should be: 'example.com'
 PASS: port should be: 'null'
+PASS: origin should be: 'app-specific://example.com'
 PASS: path should be: 'null'
 PASS: queryString should be: 'null'
 PASS: fragment should be: 'null'
@@ -135,6 +147,7 @@ PASS: scheme should be: 'http'
 PASS: userinfo should be: 'null'
 PASS: host should be: 'example'
 PASS: port should be: 'null'
+PASS: origin should be: 'http://example'
 PASS: path should be: 'null'
 PASS: queryString should be: 'null'
 PASS: fragment should be: 'null'
@@ -145,6 +158,7 @@ PASS: scheme should be: 'http'
 PASS: userinfo should be: 'null'
 PASS: host should be: 'my.example.com'
 PASS: port should be: 'null'
+PASS: origin should be: 'http://my.example.com'
 PASS: path should be: 'null'
 PASS: queryString should be: 'null'
 PASS: fragment should be: 'null'
@@ -155,6 +169,7 @@ PASS: scheme should be: 'data'
 PASS: userinfo should be: 'null'
 PASS: host should be: 'null'
 PASS: port should be: 'null'
+PASS: origin should be: 'null'
 PASS: path should be: 'null'
 PASS: queryString should be: 'null'
 PASS: fragment should be: 'null'
@@ -183,6 +198,9 @@ FAIL: host should be: 'example.com'
     Expected: "example.com"
     Actual: null
 PASS: port should be: 'null'
+FAIL: origin should be: 'http://example.com'
+    Expected: "http://example.com"
+    Actual: null
 FAIL: path should be: '/'
     Expected: "/"
     Actual: null
@@ -199,6 +217,9 @@ FAIL: host should be: 'example.com'
     Expected: "example.com"
     Actual: null
 PASS: port should be: 'null'
+FAIL: origin should be: 'http://example.com'
+    Expected: "http://example.com"
+    Actual: null
 FAIL: path should be: '/'
     Expected: "/"
     Actual: null
@@ -211,6 +232,7 @@ PASS: scheme should be: 'http'
 PASS: userinfo should be: 'user:pass'
 PASS: host should be: 'example.com'
 PASS: port should be: 'null'
+PASS: origin should be: 'http://example.com'
 PASS: path should be: '/'
 PASS: queryString should be: 'null'
 PASS: fragment should be: 'null'
@@ -225,6 +247,9 @@ FAIL: host should be: 'example.com'
     Expected: "example.com"
     Actual: null
 PASS: port should be: 'null'
+FAIL: origin should be: 'http://example.com'
+    Expected: "http://example.com"
+    Actual: null
 FAIL: path should be: '/'
     Expected: "/"
     Actual: null
@@ -239,6 +264,9 @@ FAIL: host should be: 'example.com'
     Expected: "example.com"
     Actual: "example.com?key=alpha"
 PASS: port should be: 'null'
+FAIL: origin should be: 'http://example.com'
+    Expected: "http://example.com"
+    Actual: "http://example.com?key=alpha"
 FAIL: path should be: 'null'
     Expected: null
     Actual: "/beta"
@@ -351,6 +379,64 @@ PASS: The query 'a==1=&b==2=' was parsed successfully.
 PASS: The query 'a&b=1&c==2=&d&e=3&f==4=' was parsed successfully.
 PASS: The query 'a=foo%20bar&b=123%3A456' was parsed successfully.
 
+-- Running test case: WI.displayNameForURL
+PASS: Display name of 'a' should be 'a'.
+PASS: Display name of 'http://' should be 'http://'.
+PASS: Display name of 'http://example' should be 'example'.
+PASS: Display name of 'http://example.com' should be 'example.com'.
+PASS: Display name of 'http://example.com/' should be 'example.com'.
+PASS: Display name of 'http://example.com:999999999' should be 'example.com'.
+PASS: Display name of 'http://example.com:80/' should be 'example.com'.
+PASS: Display name of 'http://example.com/path' should be 'path'.
+PASS: Display name of 'http://example.com/path/' should be 'path'.
+PASS: Display name of 'http://example.com/path/to' should be 'to'.
+PASS: Display name of 'http://example.com/path/to/' should be 'to'.
+PASS: Display name of 'http://example.com/path/to/page.html' should be 'page.html'.
+PASS: Display name of 'http://example.com/path/to/page.html?' should be 'page.html'.
+PASS: Display name of 'http://example.com/path/to/page.html?a=1' should be 'page.html'.
+PASS: Display name of 'http://example.com/path/to/page.html?a=1&b=2' should be 'page.html'.
+PASS: Display name of 'http://example.com/path/to/page.html?a=1&b=2#test' should be 'page.html'.
+PASS: Display name of 'http://example.com:123/path/to/page.html?a=1&b=2#test' should be 'page.html'.
+PASS: Display name of 'http://example.com/path/to/page.html#test' should be 'page.html'.
+PASS: Display name of 'http://example.com#alpha/beta' should be 'example.com'.
+PASS: Display name of 'http://example.com?key=alpha/beta' should be 'beta'.
+PASS: Display name of 'http://user:pass@example.com/' should be 'example.com'.
+PASS: Display name of 'http://my.example.com' should be 'my.example.com'.
+PASS: Display name of 'file://foo/bar' should be 'bar'.
+PASS: Display name of 'data:text/plain,test' should be 'data:text/plain,test'.
+PASS: Display name of 'about:blank' should be 'about:blank'.
+PASS: Display name of 'about:srcdoc' should be 'about:srcdoc'.
+PASS: Display name of 'app-specific://example.com' should be 'example.com'.
+
+Allowing directory as name...
+PASS: Display name of 'a' should be 'a'.
+PASS: Display name of 'http://' should be 'http://'.
+PASS: Display name of 'http://example' should be 'example'.
+PASS: Display name of 'http://example.com' should be 'example.com'.
+PASS: Display name of 'http://example.com/' should be '/'.
+PASS: Display name of 'http://example.com:999999999' should be 'example.com'.
+PASS: Display name of 'http://example.com:80/' should be '/'.
+PASS: Display name of 'http://example.com/path' should be 'path'.
+PASS: Display name of 'http://example.com/path/' should be '/'.
+PASS: Display name of 'http://example.com/path/to' should be 'to'.
+PASS: Display name of 'http://example.com/path/to/' should be '/'.
+PASS: Display name of 'http://example.com/path/to/page.html' should be 'page.html'.
+PASS: Display name of 'http://example.com/path/to/page.html?' should be 'page.html'.
+PASS: Display name of 'http://example.com/path/to/page.html?a=1' should be 'page.html'.
+PASS: Display name of 'http://example.com/path/to/page.html?a=1&b=2' should be 'page.html'.
+PASS: Display name of 'http://example.com/path/to/page.html?a=1&b=2#test' should be 'page.html'.
+PASS: Display name of 'http://example.com:123/path/to/page.html?a=1&b=2#test' should be 'page.html'.
+PASS: Display name of 'http://example.com/path/to/page.html#test' should be 'page.html'.
+PASS: Display name of 'http://example.com#alpha/beta' should be 'example.com'.
+PASS: Display name of 'http://example.com?key=alpha/beta' should be 'beta'.
+PASS: Display name of 'http://user:pass@example.com/' should be '/'.
+PASS: Display name of 'http://my.example.com' should be 'my.example.com'.
+PASS: Display name of 'file://foo/bar' should be 'bar'.
+PASS: Display name of 'data:text/plain,test' should be 'data:text/plain,test'.
+PASS: Display name of 'about:blank' should be 'about:blank'.
+PASS: Display name of 'about:srcdoc' should be 'about:srcdoc'.
+PASS: Display name of 'app-specific://example.com' should be 'example.com'.
+
 -- Running test case: WI.h2Authority
 PASS: HTTP/2 :authority of 'http://example.com' should be 'example.com'.
 PASS: HTTP/2 :authority of 'https://example.com' should be 'example.com'.
index 6f7a369..3fecd01 100644 (file)
@@ -27,13 +27,14 @@ function test()
                 InspectorTest.log("");
                 InspectorTest.log("Test Valid: " + url);
 
-                let {scheme: expectedScheme, userinfo: expectedUserInfo, host: expectedHost, port: expectedPort, path: expectedPath, queryString: expectedQueryString, fragment: expectedFragment, lastPathComponent: expectedLastPathComponent} = expected;
-                let {scheme: actualScheme, userinfo: actualUserInfo, host: actualHost, port: actualPort, path: actualPath, queryString: actualQueryString, fragment: actualFragment, lastPathComponent: actualLastPathComponent} = parseURL(url);
+                let {scheme: expectedScheme, userinfo: expectedUserInfo, host: expectedHost, port: expectedPort, origin: expectedOrigin, path: expectedPath, queryString: expectedQueryString, fragment: expectedFragment, lastPathComponent: expectedLastPathComponent} = expected;
+                let {scheme: actualScheme, userinfo: actualUserInfo, host: actualHost, port: actualPort, origin: actualOrigin, path: actualPath, queryString: actualQueryString, fragment: actualFragment, lastPathComponent: actualLastPathComponent} = parseURL(url);
 
                 InspectorTest.expectEqual(actualScheme, expectedScheme, `scheme should be: '${expectedScheme}'`);
                 InspectorTest.expectEqual(actualUserInfo, expectedUserInfo, `userinfo should be: '${expectedUserInfo}'`);
                 InspectorTest.expectEqual(actualHost, expectedHost, `host should be: '${expectedHost}'`);
                 InspectorTest.expectEqual(actualPort, expectedPort, `port should be: '${expectedPort}'`);
+                InspectorTest.expectEqual(actualOrigin, expectedOrigin, `origin should be: '${expectedOrigin}'`);
                 InspectorTest.expectEqual(actualPath, expectedPath, `path should be: '${expectedPath}'`);
                 InspectorTest.expectEqual(actualQueryString, expectedQueryString, `queryString should be: '${expectedQueryString}'`);
                 InspectorTest.expectEqual(actualFragment, expectedFragment, `fragment should be: '${expectedFragment}'`);
@@ -48,6 +49,7 @@ function test()
                 userinfo: null,
                 host: "example.com",
                 port: null,
+                origin: "http://example.com",
                 path: null,
                 queryString: null,
                 fragment: null,
@@ -59,6 +61,7 @@ function test()
                 userinfo: null,
                 host: "example.com",
                 port: null,
+                origin: "http://example.com",
                 path: "/",
                 queryString: null,
                 fragment: null,
@@ -70,6 +73,7 @@ function test()
                 userinfo: null,
                 host: "example.com",
                 port: 80,
+                origin: "http://example.com:80",
                 path: "/",
                 queryString: null,
                 fragment: null,
@@ -81,6 +85,7 @@ function test()
                 userinfo: null,
                 host: "example.com",
                 port: null,
+                origin: "http://example.com",
                 path: "/path/to/page.html",
                 queryString: null,
                 fragment: null,
@@ -92,6 +97,7 @@ function test()
                 userinfo: null,
                 host: "example.com",
                 port: null,
+                origin: "http://example.com",
                 path: "/path/to/page.html",
                 queryString: "",
                 fragment: null,
@@ -103,6 +109,7 @@ function test()
                 userinfo: null,
                 host: "example.com",
                 port: null,
+                origin: "http://example.com",
                 path: "/path/to/page.html",
                 queryString: "a=1",
                 fragment: null,
@@ -114,6 +121,7 @@ function test()
                 userinfo: null,
                 host: "example.com",
                 port: null,
+                origin: "http://example.com",
                 path: "/path/to/page.html",
                 queryString: "a=1&b=2",
                 fragment: null,
@@ -125,6 +133,7 @@ function test()
                 userinfo: null,
                 host: "example.com",
                 port: null,
+                origin: "http://example.com",
                 path: "/path/to/page.html",
                 queryString: "a=1&b=2",
                 fragment: "test",
@@ -136,6 +145,7 @@ function test()
                 userinfo: null,
                 host: "example.com",
                 port: 123,
+                origin: "http://example.com:123",
                 path: "/path/to/page.html",
                 queryString: "a=1&b=2",
                 fragment: "test",
@@ -147,6 +157,7 @@ function test()
                 userinfo: null,
                 host: "example.com",
                 port: null,
+                origin: "http://example.com",
                 path: "/path/to/page.html",
                 queryString: null,
                 fragment: "test",
@@ -158,6 +169,7 @@ function test()
                 userinfo: null,
                 host: "example.com",
                 port: null,
+                origin: "http://example.com",
                 path: null,
                 queryString: null,
                 fragment: "alpha/beta",
@@ -169,6 +181,7 @@ function test()
                 userinfo: null,
                 host: "example.com",
                 port: null,
+                origin: "app-specific://example.com",
                 path: null,
                 queryString: null,
                 fragment: null,
@@ -180,6 +193,7 @@ function test()
                 userinfo: null,
                 host: "example",
                 port: null,
+                origin: "http://example",
                 path: null,
                 queryString: null,
                 fragment: null,
@@ -191,6 +205,7 @@ function test()
                 userinfo: null,
                 host: "my.example.com",
                 port: null,
+                origin: "http://my.example.com",
                 path: null,
                 queryString: null,
                 fragment: null,
@@ -203,6 +218,7 @@ function test()
                 userinfo: null,
                 host: null,
                 port: null,
+                origin: null,
                 path: null,
                 queryString: null,
                 fragment: null,
@@ -221,6 +237,7 @@ function test()
                 userinfo: null,
                 host: "example.com",
                 port: null,
+                origin: "http://example.com",
                 path: "/",
                 queryString: null,
                 fragment: null,
@@ -232,6 +249,7 @@ function test()
                 userinfo: null,
                 host: "example.com",
                 port: null,
+                origin: "http://example.com",
                 path: "/",
                 queryString: null,
                 fragment: null,
@@ -243,6 +261,7 @@ function test()
                 userinfo: "user:pass",
                 host: "example.com",
                 port: null,
+                origin: "http://example.com",
                 path: "/",
                 queryString: null,
                 fragment: null,
@@ -254,6 +273,7 @@ function test()
                 userinfo: null,
                 host: "example.com",
                 port: null,
+                origin: "http://example.com",
                 path: "/",
                 queryString: null,
                 fragment: null,
@@ -265,6 +285,7 @@ function test()
                 userinfo: null,
                 host: "example.com",
                 port: null,
+                origin: "http://example.com",
                 path: null,
                 queryString: "key=alpha/beta",
                 fragment: null,
@@ -419,6 +440,50 @@ function test()
     });
 
     suite.addTestCase({
+        name: "WI.displayNameForURL",
+        test() {
+            const tests = [
+                {url: "a", expected: "a"},
+                {url: "http://", expected: "http://"},
+                {url: "http://example", expected: "example"},
+                {url: "http://example.com", expected: "example.com"},
+                {url: "http://example.com/", expected: "example.com", directory: "/"},
+                {url: "http://example.com:999999999", expected: "example.com"},
+                {url: "http://example.com:80/", expected: "example.com", directory: "/"},
+                {url: "http://example.com/path", expected: "path"},
+                {url: "http://example.com/path/", expected: "path", directory: "/"},
+                {url: "http://example.com/path/to", expected: "to"},
+                {url: "http://example.com/path/to/", expected: "to", directory: "/"},
+                {url: "http://example.com/path/to/page.html", expected: "page.html"},
+                {url: "http://example.com/path/to/page.html?", expected: "page.html"},
+                {url: "http://example.com/path/to/page.html?a=1", expected: "page.html"},
+                {url: "http://example.com/path/to/page.html?a=1&b=2", expected: "page.html"},
+                {url: "http://example.com/path/to/page.html?a=1&b=2#test", expected: "page.html"},
+                {url: "http://example.com:123/path/to/page.html?a=1&b=2#test", expected: "page.html"},
+                {url: "http://example.com/path/to/page.html#test", expected: "page.html"},
+                {url: "http://example.com#alpha/beta", expected: "example.com"},
+                {url: "http://example.com?key=alpha/beta", expected: "beta"},
+                {url: "http://user:pass@example.com/", expected: "example.com", directory: "/"},
+                {url: "http://my.example.com", expected: "my.example.com"},
+                {url: "file://foo/bar", expected: "bar"},
+                {url: "data:text/plain,test", expected: "data:text/plain,test"},
+                {url: "about:blank", expected: "about:blank"},
+                {url: "about:srcdoc", expected: "about:srcdoc"},
+                {url: "app-specific://example.com", expected: "example.com"},
+            ];
+
+            for (let {url, expected} of tests)
+                InspectorTest.expectEqual(WI.displayNameForURL(url), expected, `Display name of '${url}' should be '${expected}'.`);
+
+            InspectorTest.newline();
+
+            InspectorTest.log("Allowing directory as name...");
+            for (let {url, expected, directory} of tests)
+                InspectorTest.expectEqual(WI.displayNameForURL(url, null, {allowDirectoryAsName: true}), directory || expected, `Display name of '${url}' should be '${directory || expected}'.`);
+        },
+    });
+
+    suite.addTestCase({
         name: "WI.h2Authority",
         test() {
             function test(url, expected) {
index 7724562..0f856b0 100644 (file)
@@ -1,5 +1,75 @@
 2019-08-15  Devin Rousso  <drousso@apple.com>
 
+        Web Inspector: Sources: provide a way to create an arbitrary Inspector Style Sheet
+        https://bugs.webkit.org/show_bug.cgi?id=200425
+
+        Reviewed by Joseph Pecoraro.
+
+        Right now, the only way to create an Inspector Style Sheet is by creating a new rule in the
+        Styles sidebar of the Elements Tab. This is unnecessarily restrictive, especially for those
+        who don't use the Elements tab.
+
+        Add a + button after the filter bar in the Navigation sidebar. Clicking on the + button will
+        show a menu with the following (more likely to be added later):
+         - Inspector Style Sheet
+         - Frames (if there are subframes)
+           - (name of subframe)
+              - Inspector Style Sheet
+
+        * UserInterface/Views/SourcesNavigationSidebarPanel.js:
+        (WI.SourcesNavigationSidebarPanel):
+        (WI.SourcesNavigationSidebarPanel.prototype.treeElementForRepresentedObject): Added.
+        (WI.SourcesNavigationSidebarPanel.prototype._filterByResourcesWithIssues): Added.
+        (WI.SourcesNavigationSidebarPanel.prototype._compareTreeElements):
+        (WI.SourcesNavigationSidebarPanel.prototype._updateMainFrameTreeElement):
+        (WI.SourcesNavigationSidebarPanel.prototype._addResource):
+        (WI.SourcesNavigationSidebarPanel.prototype._handleTreeSelectionDidChange):
+        (WI.SourcesNavigationSidebarPanel.prototype._populateCreateResourceContextMenu): Added.
+        (WI.SourcesNavigationSidebarPanel.prototype._handleResourceGroupingModeChanged):
+        (WI.SourcesNavigationSidebarPanel.prototype._handleFrameWasAdded): Added.
+        (WI.SourcesNavigationSidebarPanel.prototype._handleMainFrameDidChange): Deleted.
+        * UserInterface/Views/GeneralTreeElement.js:
+        (WI.GeneralTreeElement.prototype.createFoldersAsNeededForSubpath):
+        Drive-by: sort `WI.ResourceTreeElement`s alongside `WI.FolderTreeElement`s for easier readability.
+
+        * UserInterface/Views/FrameTreeElement.js:
+        (WI.FrameTreeElement.prototype.onpopulate):
+        Add all `inspectorStyleSheetsForFrame` instead of just the preferred one so that they all
+        are visible/selectable for editing.
+
+        * UserInterface/Views/FilterBar.js:
+        (WI.FilterBar):
+        * UserInterface/Views/FilterBar.css:
+        (.filter-bar > .navigation-bar > .item):
+        (.filter-bar > input[type="search"]):
+        (.filter-bar > .navigation-bar + input[type="search"]): Added.
+        (.filter-bar > input[type="search"] + .navigation-bar:empty): Added.
+        Move the position of the filter bar buttons to be after the filter bar itself, so that other
+        parents can add action items before the filter bar to keep a consistent positioning.
+         - to the left of the filter bar are action items (e.g. "+")
+         - the filter bar itself
+         - to the right of the filter bar are filter buttons (e.g. "filter by resoure with issue")
+
+        * UserInterface/Controllers/NetworkManager.js:
+        (WI.NetworkManager.prototype.get frames):
+        Drive-by: use `Array.from`, instead of `[...map.values()]`.
+
+        * UserInterface/Models/Frame.js:
+        (WI.Frame.prototype.get url):
+        (WI.Frame.prototype.get urlComponents): Added.
+
+        * UserInterface/Base/URLUtilities.js.js:
+        (parseURL):
+        Calculate and include the `origin` string with the output.
+
+        * UserInterface/Controllers/CSSManager.js:
+        (WI.CSSManager.prototype.preferredInspectorStyleSheetForFrame):
+        Remove `doNotCreateIfMissing` now that the last caller has been removed.
+
+        * Localizations/en.lproj/localizedStrings.js:
+
+2019-08-15  Devin Rousso  <drousso@apple.com>
+
         Web Inspector: CodeMirror still inserts a tab even when "Prefer indent using" is set to "Spaces"
         https://bugs.webkit.org/show_bug.cgi?id=200770
 
index 4483237..fb89c11 100644 (file)
@@ -300,6 +300,7 @@ localizedStrings["Could not fetch properties. Object may no longer exist."] = "C
 localizedStrings["Count"] = "Count";
 localizedStrings["Create %s Rule"] = "Create %s Rule";
 localizedStrings["Create Breakpoint"] = "Create Breakpoint";
+localizedStrings["Create Resource"] = "Create Resource";
 localizedStrings["Create a new tab"] = "Create a new tab";
 localizedStrings["Cross-Origin Restrictions"] = "Cross-Origin Restrictions";
 localizedStrings["Current"] = "Current";
index fae7535..483c8ac 100644 (file)
@@ -96,11 +96,11 @@ function parseURL(url)
     url = url ? url.trim() : "";
 
     if (url.startsWith("data:"))
-        return {scheme: "data", userinfo: null, host: null, port: null, path: null, queryString: null, fragment: null, lastPathComponent: null};
+        return {scheme: "data", userinfo: null, host: null, port: null, origin: null, path: null, queryString: null, fragment: null, lastPathComponent: null};
 
     let match = url.match(/^(?<scheme>[^\/:]+):\/\/(?:(?<userinfo>[^#@\/]+)@)?(?<host>[^\/#:]*)(?::(?<port>[\d]+))?(?:(?<path>\/[^#]*)?(?:#(?<fragment>.*))?)?$/i);
     if (!match)
-        return {scheme: null, userinfo: null, host: null, port: null, path: null, queryString: null, fragment: null, lastPathComponent: null};
+        return {scheme: null, userinfo: null, host: null, port: null, origin: null, path: null, queryString: null, fragment: null, lastPathComponent: null};
 
     let scheme = match.groups.scheme.toLowerCase();
     let userinfo = match.groups.userinfo || null;
@@ -131,7 +131,14 @@ function parseURL(url)
             lastPathComponent = path.substring(lastSlashIndex + 1, path.length - endOffset);
     }
 
-    return {scheme, userinfo, host, port, path, queryString, fragment, lastPathComponent};
+    let origin = null;
+    if (scheme && host) {
+        origin = scheme + "://" + host;
+        if (port)
+            origin += ":" + port;
+    }
+
+    return {scheme, userinfo, host, port, origin, path, queryString, fragment, lastPathComponent};
 }
 
 function absoluteURL(partialURL, baseURL)
@@ -234,7 +241,7 @@ WI.displayNameForURL = function(url, urlComponents, options = {})
         displayName = urlComponents.lastPathComponent;
     }
 
-    if (options.allowDirectoryAsName && (!displayName || urlComponents.path.endsWith(displayName + "/")))
+    if (options.allowDirectoryAsName && (urlComponents.path === "/" || (displayName && urlComponents.path.endsWith(displayName + "/"))))
         displayName = "/";
 
     return displayName || WI.displayNameForHost(urlComponents.host) || url;
index dd1218e..fd0d236 100644 (file)
@@ -305,7 +305,7 @@ WI.CSSManager = class CSSManager extends WI.Object
         return this.styleSheets.filter((styleSheet) => styleSheet.isInspectorStyleSheet() && styleSheet.parentFrame === frame);
     }
 
-    preferredInspectorStyleSheetForFrame(frame, callback, doNotCreateIfMissing)
+    preferredInspectorStyleSheetForFrame(frame, callback)
     {
         var inspectorStyleSheets = this.inspectorStyleSheetsForFrame(frame);
         for (let styleSheet of inspectorStyleSheets) {
@@ -315,9 +315,6 @@ WI.CSSManager = class CSSManager extends WI.Object
             }
         }
 
-        if (doNotCreateIfMissing)
-            return;
-
         if (CSSAgent.createStyleSheet) {
             CSSAgent.createStyleSheet(frame.id, function(error, styleSheetId) {
                 const url = null;
index 093055d..7a5c9f5 100644 (file)
@@ -113,7 +113,7 @@ WI.NetworkManager = class NetworkManager extends WI.Object
 
     get frames()
     {
-        return [...this._frameIdentifierMap.values()];
+        return Array.from(this._frameIdentifierMap.values());
     }
 
     frameForIdentifier(frameId)
index a132449..3587863 100644 (file)
@@ -187,7 +187,12 @@ WI.Frame = class Frame extends WI.Object
 
     get url()
     {
-        return this._mainResource._url;
+        return this._mainResource.url;
+    }
+
+    get urlComponents()
+    {
+        return this._mainResource.urlComponents;
     }
 
     get domTree()
index f9f86ef..80bef3a 100644 (file)
@@ -40,7 +40,6 @@
 
 .filter-bar > .navigation-bar > .item {
     padding: 0 0 3px;
-    -webkit-padding-start: 8px;
 }
 
 .filter-bar > input[type="search"] {
@@ -48,7 +47,8 @@
     flex: 1;
     min-width: 0;
 
-    margin: 3px 6px 4px;
+    margin: 3px 0 4px;
+    -webkit-margin-start: 6px;
     padding-top: 0;
 
     outline: none;
     height: 22px;
 }
 
+.filter-bar > .navigation-bar + input[type="search"] {
+    -webkit-margin-start: 0;
+}
+
 .filter-bar > input[type="search"]::placeholder {
     color: hsla(0, 0%, 0%, 0.35);
 }
     animation-iteration-count: infinite;
     animation-timing-function: step-start;
 }
+
+.filter-bar > input[type="search"] + .navigation-bar:empty {
+    -webkit-margin-start: 6px;
+}
index 581daec..02c4016 100644 (file)
@@ -32,9 +32,6 @@ WI.FilterBar = class FilterBar extends WI.Object
         this._element = element || document.createElement("div");
         this._element.classList.add("filter-bar");
 
-        this._filtersNavigationBar = new WI.NavigationBar;
-        this._element.appendChild(this._filtersNavigationBar.element);
-
         this._filterFunctionsMap = new Map;
 
         this._inputField = document.createElement("input");
@@ -46,6 +43,9 @@ WI.FilterBar = class FilterBar extends WI.Object
         this._inputField.addEventListener("input", this._handleFilterInputEvent.bind(this));
         this._element.appendChild(this._inputField);
 
+        this._filtersNavigationBar = new WI.NavigationBar;
+        this._element.appendChild(this._filtersNavigationBar.element);
+
         this._lastFilterValue = this.filters;
     }
 
index 4116a5a..06be8fe 100644 (file)
@@ -178,8 +178,8 @@ WI.FrameTreeElement = class FrameTreeElement extends WI.ResourceTreeElement
                 this.addChildForRepresentedObject(extraScript);
         }
 
-        const doNotCreateIfMissing = true;
-        WI.cssManager.preferredInspectorStyleSheetForFrame(this._frame, this.addRepresentedObjectToNewChildQueue.bind(this), doNotCreateIfMissing);
+        for (let styleSheet of WI.cssManager.inspectorStyleSheetsForFrame(this._frame))
+            this.addChildForRepresentedObject(styleSheet);
     }
 
     onexpand()
index c44d62c..58ac1f9 100644 (file)
@@ -191,7 +191,7 @@ WI.GeneralTreeElement = class GeneralTreeElement extends WI.TreeElement
         this._tooltipHandledSeparately = !!x;
     }
 
-    createFoldersAsNeededForSubpath(subpath)
+    createFoldersAsNeededForSubpath(subpath, comparator)
     {
         if (!subpath)
             return this;
@@ -223,7 +223,7 @@ WI.GeneralTreeElement = class GeneralTreeElement extends WI.TreeElement
             let newFolder = new WI.FolderTreeElement(component);
             this._subpathFolderTreeElementMap.set(currentPath, newFolder);
 
-            let index = insertionIndexForObjectInListSortedByFunction(newFolder, currentFolderTreeElement.children, WI.ResourceTreeElement.compareFolderAndResourceTreeElements);
+            let index = insertionIndexForObjectInListSortedByFunction(newFolder, currentFolderTreeElement.children, comparator || WI.ResourceTreeElement.compareFolderAndResourceTreeElements);
             currentFolderTreeElement.insertChild(newFolder, index);
             currentFolderTreeElement = newFolder;
         }
index aa0c0f8..b4cd1bd 100644 (file)
@@ -242,23 +242,18 @@ WI.SourcesNavigationSidebarPanel = class SourcesNavigationSidebarPanel extends W
         this._resourcesTreeOutline.includeSourceMapResourceChildren = true;
         resourcesContainer.appendChild(this._resourcesTreeOutline.element);
 
-        let onlyShowResourcesWithIssuesFilterFunction = (treeElement) => {
-            if (treeElement.treeOutline !== this._resourcesTreeOutline)
-                return true;
+        if (InspectorBackend.domains.CSS) {
+            let createResourceNavigationBar = new WI.NavigationBar;
 
-            if (treeElement instanceof WI.IssueTreeElement)
-                return true;
+            let createResourceButtonNavigationItem = new WI.ButtonNavigationItem("create-resource", WI.UIString("Create Resource"), "Images/Plus15.svg", 15, 15);
+            WI.addMouseDownContextMenuHandlers(createResourceButtonNavigationItem.element, this._populateCreateResourceContextMenu.bind(this));
+            createResourceNavigationBar.addNavigationItem(createResourceButtonNavigationItem);
+
+            this.filterBar.element.insertBefore(createResourceNavigationBar.element, this.filterBar.element.firstChild);
+        }
 
-            if (treeElement.hasChildren) {
-                for (let child of treeElement.children) {
-                    if (child instanceof WI.IssueTreeElement)
-                        return true;
-                }
-            }
-            return false;
-        };
         const activatedByDefault = false;
-        this.filterBar.addFilterBarButton("sources-only-show-resources-with-issues", onlyShowResourcesWithIssuesFilterFunction, activatedByDefault, WI.UIString("Only show resources with issues"), WI.UIString("Show all resources"), "Images/Errors.svg", 15, 15);
+        this.filterBar.addFilterBarButton("sources-only-show-resources-with-issues", this._filterByResourcesWithIssues.bind(this), activatedByDefault, WI.UIString("Only show resources with issues"), WI.UIString("Show all resources"), "Images/Errors.svg", 15, 15);
 
         WI.settings.resourceGroupingMode.addEventListener(WI.Setting.Event.Changed, this._handleResourceGroupingModeChanged, this);
 
@@ -266,7 +261,7 @@ WI.SourcesNavigationSidebarPanel = class SourcesNavigationSidebarPanel extends W
         WI.Frame.addEventListener(WI.Frame.Event.ResourceWasAdded, this._handleResourceAdded, this);
         WI.Target.addEventListener(WI.Target.Event.ResourceAdded, this._handleResourceAdded, this);
 
-        WI.networkManager.addEventListener(WI.NetworkManager.Event.MainFrameDidChange, this._handleMainFrameDidChange, this);
+        WI.networkManager.addEventListener(WI.NetworkManager.Event.FrameWasAdded, this._handleFrameWasAdded, this);
 
         WI.debuggerManager.addEventListener(WI.DebuggerManager.Event.BreakpointAdded, this._handleDebuggerBreakpointAdded, this);
         WI.domDebuggerManager.addEventListener(WI.DOMDebuggerManager.Event.DOMBreakpointAdded, this._handleDebuggerBreakpointAdded, this);
@@ -429,8 +424,21 @@ WI.SourcesNavigationSidebarPanel = class SourcesNavigationSidebarPanel extends W
             return null;
         }
 
-        if (representedObject instanceof WI.Resource && representedObject.parentFrame && representedObject.parentFrame.mainResource === representedObject)
-            representedObject = representedObject.parentFrame;
+        switch (WI.settings.resourceGroupingMode.value) {
+        case WI.Resource.GroupingMode.Path:
+            if (representedObject instanceof WI.Frame)
+                representedObject = representedObject.mainResource;
+            break;
+
+        default:
+            WI.reportInternalError("Unknown resource grouping mode", {"Resource Grouping Mode": WI.settings.resourceGroupingMode.value});
+            // Fallthrough for default value.
+
+        case WI.Resource.GroupingMode.Type:
+            if (representedObject instanceof WI.Resource && representedObject.parentFrame && representedObject.parentFrame.mainResource === representedObject)
+                representedObject = representedObject.parentFrame;
+            break;
+        }
 
         function isAncestor(ancestor, resourceOrFrame) {
             // SourceMapResources are descendants of another SourceCode object.
@@ -597,6 +605,23 @@ WI.SourcesNavigationSidebarPanel = class SourcesNavigationSidebarPanel extends W
 
     // Private
 
+    _filterByResourcesWithIssues(treeElement)
+    {
+        if (treeElement.treeOutline !== this._resourcesTreeOutline)
+            return true;
+
+        if (treeElement instanceof WI.IssueTreeElement)
+            return true;
+
+        if (treeElement.hasChildren) {
+            for (let child of treeElement.children) {
+                if (child instanceof WI.IssueTreeElement)
+                    return true;
+            }
+        }
+        return false;
+    }
+
     _compareTreeElements(a, b)
     {
         const rankFunctions = [
@@ -604,8 +629,7 @@ WI.SourcesNavigationSidebarPanel = class SourcesNavigationSidebarPanel extends W
             (treeElement) => treeElement === this._mainFrameTreeElement,
             (treeElement) => treeElement instanceof WI.FrameTreeElement,
             (treeElement) => {
-                return treeElement instanceof WI.FolderTreeElement
-                    && treeElement !== this._extensionScriptsFolderTreeElement
+                return treeElement !== this._extensionScriptsFolderTreeElement
                     && treeElement !== this._extraScriptsFolderTreeElement
                     && treeElement !== this._anonymousScriptsFolderTreeElement;
             },
@@ -638,18 +662,19 @@ WI.SourcesNavigationSidebarPanel = class SourcesNavigationSidebarPanel extends W
         if (!mainFrame)
             return;
 
-        let resourceGroupingMode = WI.settings.resourceGroupingMode.value;
-        switch (resourceGroupingMode) {
-        case WI.Resource.GroupingMode.Path:
+        switch (WI.settings.resourceGroupingMode.value) {
+        case WI.Resource.GroupingMode.Path: {
             for (let treeElement of this._originTreeElementMap.values()) {
                 if (treeElement !== oldMainFrameTreeElement)
                     this._resourcesTreeOutline.removeChild(treeElement);
             }
             this._originTreeElementMap.clear();
 
-            this._mainFrameTreeElement = new WI.FolderTreeElement(mainFrame.securityOrigin, mainFrame);
-            this._originTreeElementMap.set(mainFrame.securityOrigin, this._mainFrameTreeElement);
+            let origin = mainFrame.urlComponents.origin;
+            this._mainFrameTreeElement = new WI.FolderTreeElement(origin);
+            this._originTreeElementMap.set(origin, this._mainFrameTreeElement);
             break;
+        }
 
         default:
             WI.reportInternalError("Unknown resource grouping mode", {"Resource Grouping Mode": WI.settings.resourceGroupingMode.value});
@@ -711,32 +736,30 @@ WI.SourcesNavigationSidebarPanel = class SourcesNavigationSidebarPanel extends W
             if (!this._mainFrameTreeElement || this._resourcesTreeOutline.findTreeElement(resource))
                 return;
 
-            let origin = null;
-            if (resource.urlComponents.scheme && resource.urlComponents.host) {
-                origin = resource.urlComponents.scheme + "://" + resource.urlComponents.host;
-                if (resource.urlComponents.port)
-                    origin += ":" + resource.urlComponents.port;
-            } else if (resource.parentFrame)
-                origin = resource.parentFrame.securityOrigin;
-
             let parentTreeElement = null;
-            if (origin) {
-                let frameTreeElement = this._originTreeElementMap.get(origin);
-                if (!frameTreeElement) {
-                    frameTreeElement = new WI.FolderTreeElement(origin, origin === resource.parentFrame.securityOrigin ? resource.parentFrame : null);
-                    this._originTreeElementMap.set(origin, frameTreeElement);
-
-                    let index = insertionIndexForObjectInListSortedByFunction(frameTreeElement, this._resourcesTreeOutline.children, this._boundCompareTreeElements);
-                    this._resourcesTreeOutline.insertChild(frameTreeElement, index);
-                }
 
-                let subpath = resource.urlComponents.path;
-                if (subpath && subpath[0] === "/")
-                    subpath = subpath.substring(1);
+            if (resource instanceof WI.CSSStyleSheet && resource.isInspectorStyleSheet())
+                parentTreeElement = this._resourcesTreeOutline.findTreeElement(resource.parentFrame.mainResource);
 
-                parentTreeElement = frameTreeElement.createFoldersAsNeededForSubpath(subpath);
-            } else {
-                parentTreeElement = this._resourcesTreeOutline;
+            if (!parentTreeElement) {
+                let origin = resource.urlComponents.origin;
+                if (origin) {
+                    let frameTreeElement = this._originTreeElementMap.get(origin);
+                    if (!frameTreeElement) {
+                        frameTreeElement = new WI.FolderTreeElement(origin);
+                        this._originTreeElementMap.set(origin, frameTreeElement);
+
+                        let index = insertionIndexForObjectInListSortedByFunction(frameTreeElement, this._resourcesTreeOutline.children, this._boundCompareTreeElements);
+                        this._resourcesTreeOutline.insertChild(frameTreeElement, index);
+                    }
+
+                    let subpath = resource.urlComponents.path;
+                    if (subpath && subpath[0] === "/")
+                        subpath = subpath.substring(1);
+
+                    parentTreeElement = frameTreeElement.createFoldersAsNeededForSubpath(subpath, this._boundCompareTreeElements);
+                } else
+                    parentTreeElement = this._resourcesTreeOutline;
             }
 
             let resourceTreeElement = null;
@@ -1505,7 +1528,7 @@ WI.SourcesNavigationSidebarPanel = class SourcesNavigationSidebarPanel extends W
             || treeElement instanceof WI.ScriptTreeElement
             || treeElement instanceof WI.CSSStyleSheetTreeElement) {
             let representedObject = treeElement.representedObject;
-            if (representedObject instanceof WI.Collection || representedObject instanceof WI.SourceCode)
+            if (representedObject instanceof WI.Collection || representedObject instanceof WI.SourceCode || representedObject instanceof WI.Frame)
                 WI.showRepresentedObject(representedObject);
             return;
         }
@@ -1646,6 +1669,42 @@ WI.SourcesNavigationSidebarPanel = class SourcesNavigationSidebarPanel extends W
         }
     }
 
+    _populateCreateResourceContextMenu(contextMenu)
+    {
+        if (InspectorBackend.domains.CSS) {
+            let addInspectorStyleSheetItem = (menu, frame) => {
+                menu.appendItem(WI.UIString("Inspector Style Sheet"), () => {
+                    if (WI.settings.resourceGroupingMode.value === WI.Resource.GroupingMode.Path) {
+                        // Force the parent to populate.
+                        let parentFrameTreeElement = this._resourcesTreeOutline.findTreeElement(frame.mainResource);
+                        parentFrameTreeElement.reveal();
+                        parentFrameTreeElement.expand();
+                    }
+
+                    WI.cssManager.preferredInspectorStyleSheetForFrame(frame, (styleSheet) => {
+                        WI.showRepresentedObject(styleSheet);
+                    });
+                });
+            };
+
+            addInspectorStyleSheetItem(contextMenu, WI.networkManager.mainFrame);
+
+            let frames = WI.networkManager.frames;
+            if (frames.length > 2) {
+                let framesSubMenu = contextMenu.appendSubMenuItem(WI.UIString("Frames"));
+
+                for (let frame of frames) {
+                    if (frame === WI.networkManager.mainFrame || frame.mainResource.type !== WI.Resource.Type.Document)
+                        continue;
+
+                    let frameSubMenuItem = framesSubMenu.appendSubMenuItem(frame.name ? WI.UIString("%s (%s)").format(frame.name, frame.mainResource.displayName) : frame.mainResource.displayName);
+
+                    addInspectorStyleSheetItem(frameSubMenuItem, frame);
+                }
+            }
+        }
+    }
+
     _handleResourceGroupingModeChanged(event)
     {
         this._workerTargetTreeElementMap.clear();
@@ -1662,6 +1721,11 @@ WI.SourcesNavigationSidebarPanel = class SourcesNavigationSidebarPanel extends W
         if (mainFrame) {
             this._updateMainFrameTreeElement(mainFrame);
             this._addResourcesRecursivelyForFrame(mainFrame);
+
+            for (let frame of WI.networkManager.frames) {
+                if (frame !== mainFrame)
+                    this._addResourcesRecursivelyForFrame(frame);
+            }
         }
 
         for (let script of WI.debuggerManager.knownNonResourceScripts) {
@@ -1695,11 +1759,14 @@ WI.SourcesNavigationSidebarPanel = class SourcesNavigationSidebarPanel extends W
         this._addResource(event.data.resource);
     }
 
-    _handleMainFrameDidChange(event)
+    _handleFrameWasAdded(event)
     {
-        let mainFrame = WI.networkManager.mainFrame;
-        this._updateMainFrameTreeElement(mainFrame);
-        this._addResourcesRecursivelyForFrame(mainFrame);
+        let {frame} = event.data;
+
+        if (frame.isMainFrame())
+            this._updateMainFrameTreeElement(frame);
+
+        this._addResourcesRecursivelyForFrame(frame);
     }
 
     _handleDebuggerBreakpointAdded(event)