|
6 | 6 | <script src='/resources/testharnessreport.js'></script> |
7 | 7 | <script src='./support/helpers.js'></script> |
8 | 8 | <body> |
| 9 | +<script type="importmap"> |
| 10 | +{ |
| 11 | + "imports": { |
| 12 | + "blocked-by-null": null |
| 13 | + } |
| 14 | +} |
| 15 | +</script> |
9 | 16 | <script type="module"> |
| 17 | + // A failed CSS module fetch leaves its synchronously-adopted empty sheet |
| 18 | + // in `adoptedStyleSheets`. There is no observable cleanup when the fetch |
| 19 | + // fails: the empty sheet remains, but the module map's entry is sticky- |
| 20 | + // failed so subsequent imperative imports of the same URL still reject |
| 21 | + // and subsequent declarative consumers contribute no sheet for that URL. |
| 22 | + |
10 | 23 | promise_test(async (t) => { |
11 | 24 | // --- Scenario 1: a specifier that resolves to a 404 URL. --- |
12 | | - // The fetch should fail gracefully; the placeholder sheet should remain |
13 | | - // empty and no crash should occur. |
| 25 | + // Synchronously, an empty sheet is adopted while the fetch is pending. |
| 26 | + // Once the fetch fails, that empty sheet is left behind as-is. |
14 | 27 | const nonexistentUrl = "./support/nonexistent.css"; |
15 | | - const { shadowRoot, testElement } = createStylesheetHost(nonexistentUrl); |
| 28 | + const { shadowRoot } = createStylesheetHost(nonexistentUrl); |
16 | 29 |
|
17 | 30 | assert_equals(shadowRoot.adoptedStyleSheets.length, 1, |
18 | | - "Before fetch settles: expected 1 placeholder(s)."); |
19 | | - assert_equals(shadowRoot.adoptedStyleSheets[0].cssRules.length, 0, |
20 | | - "Before fetch settles: placeholder at index 0 should be empty."); |
| 31 | + "Before fetch settles: expected 1 (empty) sheet."); |
| 32 | + const placeholder = shadowRoot.adoptedStyleSheets[0]; |
| 33 | + assert_equals(placeholder.cssRules.length, 0, |
| 34 | + "Before fetch settles: sheet at index 0 should be empty."); |
21 | 35 |
|
22 | 36 | await fetchAndWait(nonexistentUrl); |
23 | 37 |
|
24 | 38 | assert_equals(shadowRoot.adoptedStyleSheets.length, 1, |
25 | | - "After failed fetch: expected 1 placeholder(s)."); |
| 39 | + "After failed fetch: empty sheet remains in adoptedStyleSheets."); |
| 40 | + assert_equals(shadowRoot.adoptedStyleSheets[0], placeholder, |
| 41 | + "After failed fetch: sheet identity is unchanged."); |
26 | 42 | assert_equals(shadowRoot.adoptedStyleSheets[0].cssRules.length, 0, |
27 | | - "After failed fetch: placeholder at index 0 should be empty."); |
| 43 | + "After failed fetch: sheet remains empty."); |
| 44 | + }, "Async fetch failure: empty placeholder sheet remains in adoptedStyleSheets."); |
28 | 45 |
|
| 46 | + promise_test(async (t) => { |
29 | 47 | // --- Scenario 2: mixed valid and invalid specifiers. --- |
30 | | - const nonexistentUrl2 = "./support/nonexistent-2.css"; |
| 48 | + // The valid sheet is populated; the invalid one stays empty. Both |
| 49 | + // entries remain in the array. |
| 50 | + const nonexistentUrl = "./support/nonexistent-2.css"; |
31 | 51 | const validUrl = "./support/styles.css?failure"; |
32 | | - const { shadowRoot: shadowRoot2, testElement: testElement2 } = |
33 | | - createStylesheetHost([nonexistentUrl2, validUrl]); |
| 52 | + const { shadowRoot, testElement } = |
| 53 | + createStylesheetHost([nonexistentUrl, validUrl]); |
34 | 54 |
|
35 | | - assert_equals(shadowRoot2.adoptedStyleSheets.length, 2, |
36 | | - "Two entries should be present (one placeholder, one fetched or placeholder)."); |
| 55 | + assert_equals(shadowRoot.adoptedStyleSheets.length, 2, |
| 56 | + "Two entries should be present synchronously (both empty pre-fetch)."); |
37 | 57 |
|
38 | | - await fetchAndWait(nonexistentUrl2, validUrl); |
| 58 | + await fetchAndWait(nonexistentUrl, validUrl); |
39 | 59 |
|
40 | | - assert_equals(shadowRoot2.adoptedStyleSheets.length, 2, |
41 | | - "adoptedStyleSheets should still have two entries."); |
42 | | - assert_equals(shadowRoot2.adoptedStyleSheets[0].cssRules.length, 0, |
43 | | - "The first entry (failed fetch) should be an empty placeholder."); |
44 | | - assertSheetRule(shadowRoot2, 1, "span { color: blue; }", "Second entry (valid)"); |
45 | | - assert_equals(getComputedStyle(testElement2).color, "rgb(0, 0, 255)", |
| 60 | + assert_equals(shadowRoot.adoptedStyleSheets.length, 2, |
| 61 | + "After settled: both sheets remain (failed and valid)."); |
| 62 | + assert_equals(shadowRoot.adoptedStyleSheets[0].cssRules.length, 0, |
| 63 | + "Failed entry remains empty."); |
| 64 | + assertSheetRule(shadowRoot, 1, "span { color: blue; }", |
| 65 | + "Valid entry"); |
| 66 | + assert_equals(getComputedStyle(testElement).color, "rgb(0, 0, 255)", |
46 | 67 | "The valid specifier's styles (blue) should be applied."); |
47 | | - }, "Async fetch failure: 404 leaves placeholder empty, valid specifier still works."); |
| 68 | + }, "Async fetch failure: failed sheet remains empty; valid specifier still works."); |
| 69 | + |
| 70 | + promise_test(async (t) => { |
| 71 | + // JS may insert additional references to the synchronously-created |
| 72 | + // placeholder before the fetch settles. Because nothing removes the |
| 73 | + // placeholder on failure, all duplicate references remain. |
| 74 | + const nonexistentUrl = "./support/nonexistent-3.css"; |
| 75 | + const { shadowRoot } = createStylesheetHost(nonexistentUrl); |
| 76 | + |
| 77 | + assert_equals(shadowRoot.adoptedStyleSheets.length, 1, |
| 78 | + "Sanity: declarative path produced one (empty) sheet synchronously."); |
| 79 | + const declarativeSheet = shadowRoot.adoptedStyleSheets[0]; |
| 80 | + |
| 81 | + shadowRoot.adoptedStyleSheets = [ |
| 82 | + declarativeSheet, declarativeSheet, declarativeSheet]; |
| 83 | + assert_equals(shadowRoot.adoptedStyleSheets.length, 3, |
| 84 | + "Sanity: JS-duplicated references produced three entries."); |
| 85 | + |
| 86 | + await fetchAndWait(nonexistentUrl); |
| 87 | + |
| 88 | + assert_equals(shadowRoot.adoptedStyleSheets.length, 3, |
| 89 | + "After failed fetch: all duplicate references remain."); |
| 90 | + for (let i = 0; i < 3; ++i) { |
| 91 | + assert_equals(shadowRoot.adoptedStyleSheets[i], declarativeSheet, |
| 92 | + `Entry ${i} should still reference the original empty sheet.`); |
| 93 | + assert_equals(shadowRoot.adoptedStyleSheets[i].cssRules.length, 0, |
| 94 | + `Entry ${i} should still be empty.`); |
| 95 | + } |
| 96 | + }, "Async fetch failure: JS-duplicated references all remain after failure."); |
| 97 | + |
| 98 | + promise_test(async (t) => { |
| 99 | + // A bare module specifier (one that is not a relative URL and has no |
| 100 | + // matching import map entry) cannot be resolved, so no fetch is |
| 101 | + // initiated and no sheet is contributed for that specifier. |
| 102 | + // adoptedStyleSheets remains empty for the host. A subsequent imperative |
| 103 | + // `import()` of the same specifier should also reject. |
| 104 | + const bareSpecifier = "bare"; |
| 105 | + const { shadowRoot } = createStylesheetHost(bareSpecifier); |
| 106 | + |
| 107 | + assert_equals(shadowRoot.adoptedStyleSheets.length, 0, |
| 108 | + "Bare specifier contributes no sheet to adoptedStyleSheets."); |
| 109 | + |
| 110 | + await promise_rejects_js(t, TypeError, |
| 111 | + import(bareSpecifier, { with: { type: "css" } }), |
| 112 | + "Imperative import of a bare specifier should reject."); |
| 113 | + |
| 114 | + assert_equals(shadowRoot.adoptedStyleSheets.length, 0, |
| 115 | + "After import rejection: still no sheet adopted."); |
| 116 | + }, "Async fetch failure: bare module specifier contributes no sheet."); |
| 117 | + |
| 118 | + promise_test(async (t) => { |
| 119 | + // An invalid URL (containing a lone surrogate, which is not valid in |
| 120 | + // any UTF-8-encoded URL) cannot be parsed, so module specifier |
| 121 | + // resolution fails for the same reason as the bare-specifier case |
| 122 | + // above. No fetch is initiated and no sheet is contributed. |
| 123 | + const invalidUrl = "\uD800"; // Lone leading surrogate, no low surrogate. |
| 124 | + const { shadowRoot } = createStylesheetHost(invalidUrl); |
| 125 | + |
| 126 | + assert_equals(shadowRoot.adoptedStyleSheets.length, 0, |
| 127 | + "Invalid URL (lone surrogate) contributes no sheet to " + |
| 128 | + "adoptedStyleSheets."); |
| 129 | + |
| 130 | + await promise_rejects_js(t, TypeError, |
| 131 | + import(invalidUrl, { with: { type: "css" } }), |
| 132 | + "Imperative import of an invalid URL (lone surrogate) should reject."); |
| 133 | + |
| 134 | + assert_equals(shadowRoot.adoptedStyleSheets.length, 0, |
| 135 | + "After import rejection: still no sheet adopted."); |
| 136 | + }, "Async fetch failure: invalid URL (lone surrogate) contributes no sheet."); |
| 137 | + |
| 138 | + promise_test(async (t) => { |
| 139 | + // The specifier here is by itself a valid bare specifier, AND the |
| 140 | + // import map declared at the top of the document does have an entry |
| 141 | + // for it -- but that entry is `null`, which the HTML spec defines as |
| 142 | + // "blocked by a null entry" and surfaces to ResolveModuleSpecifier as |
| 143 | + // an invalid resolved URL. This should be skipped for |
| 144 | + // shadowrootadoptedstylesheets processing (no fetch, no sheet |
| 145 | + // contributed). A subsequent imperative `import()` of the same specifier |
| 146 | + // rejects with TypeError. |
| 147 | + const blockedSpecifier = "blocked-by-null"; |
| 148 | + const { shadowRoot } = createStylesheetHost(blockedSpecifier); |
| 149 | + |
| 150 | + assert_equals(shadowRoot.adoptedStyleSheets.length, 0, |
| 151 | + "Specifier blocked by null import map entry contributes no sheet."); |
| 152 | + |
| 153 | + await promise_rejects_js(t, TypeError, |
| 154 | + import(blockedSpecifier, { with: { type: "css" } }), |
| 155 | + "Imperative import of a specifier mapped to null should reject."); |
| 156 | + |
| 157 | + assert_equals(shadowRoot.adoptedStyleSheets.length, 0, |
| 158 | + "After import rejection: still no sheet adopted."); |
| 159 | + }, "Async fetch failure: import map mapping specifier to null contributes " + |
| 160 | + "no sheet."); |
48 | 161 | </script> |
49 | 162 | </body> |
0 commit comments