| <!doctype html> |
| <meta charset="utf-8" /> |
| <title>HTML partial updates - streamAppendHTML</title> |
| <link rel="help" href="https://github.com/WICG/declarative-partial-updates" /> |
| <script src="/resources/testharness.js"></script> |
| <script src="/resources/testharnessreport.js"></script> |
| <style id="style"></style> |
| <p id="target"></p> |
| <div id="container"></div> |
| <script> |
| promise_test(async (t) => { |
| const placeholder = document.createElement("div"); |
| placeholder.innerHTML = "A"; |
| const writable = placeholder.streamAppendHTML(); |
| assert_true( |
| writable instanceof WritableStream, |
| "node.streamAppendHTML() returns a writable stream", |
| ); |
| const response = new Response("<span>B</span>", { |
| headers: { "Content-Type": "text/html" }, |
| }); |
| await response.body.pipeThrough(new TextDecoderStream()).pipeTo(writable); |
| assert_equals(placeholder.textContent, "AB"); |
| assert_true(response.bodyUsed); |
| }, "piping a response into streamAppendHTML()"); |
| |
| promise_test(async (t) => { |
| const placeholder = document.createElement("div"); |
| placeholder.innerHTML = "<span>A</span>"; |
| const writable = placeholder.streamAppendHTML(); |
| assert_true( |
| writable instanceof WritableStream, |
| "node.streamAppendHTML() returns a writable stream", |
| ); |
| const writer = writable.getWriter(); |
| await writer.write("B"); |
| await writer.close(); |
| assert_equals(placeholder.textContent, "AB"); |
| }, "writing a string to streamAppendHTML()"); |
| |
| promise_test(async (t) => { |
| const placeholder = document.createElement("div"); |
| placeholder.innerHTML = "<span>A</span>"; |
| const writable = placeholder.streamAppendHTML(); |
| assert_true( |
| writable instanceof WritableStream, |
| "node.streamAppendHTML() returns a writable stream", |
| ); |
| assert_equals(placeholder.textContent, "A"); |
| const writer = writable.getWriter(); |
| await writer.write("B"); |
| await writer.close(); |
| assert_equals(placeholder.textContent, "AB"); |
| }, "streamAppendHTML() should append to existing content"); |
| |
| promise_test(async (t) => { |
| const placeholder = document.createElement("div"); |
| placeholder.innerHTML = "<span>A</span>"; |
| const writable = placeholder.streamAppendHTML(); |
| assert_true( |
| writable instanceof WritableStream, |
| "node.streamAppendHTML() returns a writable stream", |
| ); |
| const writer = writable.getWriter(); |
| await writer.write("<span>B</span>"); |
| assert_equals(placeholder.textContent, "AB"); |
| await writer.close(); |
| }, "streamAppendHTML() does not buffer until close()"); |
| |
| promise_test(async (t) => { |
| const placeholder = document.createElement("div"); |
| placeholder.innerHTML = "Before"; |
| const writable = placeholder.streamAppendHTML(); |
| assert_true( |
| writable instanceof WritableStream, |
| "streamAppendHTML() returns a writable stream", |
| ); |
| const writer = writable.getWriter(); |
| await promise_rejects_js(t, TypeError, writer.write(Symbol("sym"))); |
| assert_equals(placeholder.textContent, "Before"); |
| }, "writing a Symbol to streamAppendHTML()"); |
| |
| promise_test(async (t) => { |
| const placeholder = document.createElement("div"); |
| placeholder.innerHTML = "12"; |
| const writable = placeholder.streamAppendHTML(); |
| assert_true( |
| writable instanceof WritableStream, |
| "node.streamAppendHTML() returns a writable stream", |
| ); |
| const writer = writable.getWriter(); |
| await writer.write(345); |
| await writer.close(); |
| assert_equals(placeholder.textContent, "12345"); |
| assert_equals(placeholder.childNodes.length, 1); |
| assert_equals(placeholder.firstChild.nodeType, Node.TEXT_NODE); |
| assert_equals(placeholder.firstChild.textContent, "12345"); |
| }, "writing numbers to streamAppendHTML()"); |
| |
| promise_test(async (t) => { |
| const placeholder = document.createElement("div"); |
| placeholder.innerHTML = "start "; |
| const writable = placeholder.streamAppendHTML(); |
| assert_true( |
| writable instanceof WritableStream, |
| "node.streamAppendHTML() returns a writable stream", |
| ); |
| const writer = writable.getWriter(); |
| await writer.write(null); |
| await writer.write(" "); |
| await writer.write(undefined); |
| await writer.close(); |
| assert_equals(placeholder.textContent, "start null undefined"); |
| }, "writing null or undefined to streamAppendHTML()"); |
| |
| promise_test(async (t) => { |
| const style = document.querySelector("#style"); |
| const writable = style.streamAppendHTML(); |
| assert_true( |
| writable instanceof WritableStream, |
| "node.streamAppendHTML() returns a writable stream", |
| ); |
| const writer = await writable.getWriter(); |
| await writer.write("#target { color: rgba(100, 0, 100); }"); |
| await writer.close(); |
| assert_equals( |
| getComputedStyle(document.querySelector("#target")).color, |
| "rgb(100, 0, 100)", |
| ); |
| }, "streamAppendHTML() can stream into <style>"); |
| |
| promise_test(async (t) => { |
| const style = document.createElement("style"); |
| style.innerHTML = "#target { color: rgba"; |
| const writable = style.streamAppendHTML(); |
| assert_true( |
| writable instanceof WritableStream, |
| "node.streamAppendHTML() returns a writable stream", |
| ); |
| const writer = await writable.getWriter(); |
| await writer.write("(100, 0, 100); }"); |
| await writer.close(); |
| assert_equals( |
| getComputedStyle(document.querySelector("#target")).color, |
| "rgb(100, 0, 100)", |
| ); |
| }, "streamAppendHTML() can stream into a non-empty <style>"); |
| |
| promise_test(async (t) => { |
| const placeholder = document.createElement("div"); |
| placeholder.innerHTML = "start "; |
| const writable = placeholder.streamAppendHTML(); |
| const writer = writable.getWriter(); |
| await writer.write("ABC"); |
| await writer.abort("abort-reason"); |
| await writer.write("DEF").catch(() => {}); |
| assert_equals(placeholder.textContent, "start ABC"); |
| }, "Aborting streamAppendHTML()"); |
| |
| promise_test(async (t) => { |
| const placeholder = document.createElement("div"); |
| placeholder.innerHTML = "before"; |
| const writer1 = placeholder.streamAppendHTML().getWriter(); |
| const writer2 = placeholder.streamAppendHTML().getWriter(); |
| await writer1.write("<span>A</span>"); |
| await writer2.write("<span>B</span>"); |
| await new Promise((resolve) => requestAnimationFrame(resolve)); |
| await writer1.write("C"); |
| await writer1.close(); |
| await writer2.close(); |
| assert_equals(placeholder.textContent, "beforeABC"); |
| }, "Interleaved streamAppendHTML() writes"); |
| |
| promise_test(async (t) => { |
| const element = document.createElement("div"); |
| document.body.append(element); |
| window.did_eval_script = false; |
| const writer = element.streamAppendHTML({ runScripts: true }).getWriter(); |
| assert_false(window.did_eval_script); |
| await writer.write("<script>window.did_eval_script = true;"); |
| assert_false(window.did_eval_script); |
| await writer.write("<"); |
| assert_false(window.did_eval_script); |
| await writer.write("/script>"); |
| assert_false(window.did_eval_script); |
| }, "streamAppendHTML should not execute scripts when connected"); |
| |
| promise_test(async (t) => { |
| const element = document.createElement("div"); |
| element.innerHTML = "<p>before;</p>"; |
| document.body.append(element); |
| const writer = element |
| .streamAppendHTML({ sanitizer: { removeElements: ["p"] } }) |
| .getWriter(); |
| await writer.write("<p>forbidden</p>"); |
| await writer.write("<span>allowed</span>"); |
| await writer.close(); |
| assert_equals(element.textContent, "before;allowed"); |
| }, "streamAppendHTML should pass sanitizer options"); |
| |
| promise_test(async (t) => { |
| const element = document.createElement("div"); |
| const shadow = element.attachShadow({mode: "open"}); |
| shadow.innerHTML = "<p>before;</p>"; |
| document.body.append(element); |
| const writer = shadow |
| .streamAppendHTML({ sanitizer: { removeElements: ["p"] } }) |
| .getWriter(); |
| await writer.write("<p>forbidden</p>"); |
| await writer.write("<span>allowed</span>"); |
| await writer.close(); |
| assert_equals(shadow.textContent, "before;allowed"); |
| |
| }, "streamAppendHTML on ShadowRoot"); |
| promise_test(async (t) => { |
| const element = document.createElement("div"); |
| element.innerHTML = "before;"; |
| document.body.append(element); |
| const writer = element.streamAppendHTML().getWriter(); |
| await writer.write("<script id=forbidden><" + "/script><span id=after>after</span>"); |
| await writer.close(); |
| assert_equals(element.querySelector("script"), null); |
| assert_equals(element.textContent, "before;after"); |
| }, "streamAppendHTML uses safe sanitizer"); |
| |
| promise_test(async (t) => { |
| const element = document.createElement("div"); |
| const shadow = element.attachShadow({mode: "open"}); |
| document.body.append(element); |
| const writer = shadow.streamAppendHTML().getWriter(); |
| await writer.write("<script id=forbidden<" + "/script>"); |
| await writer.close(); |
| assert_equals(shadow.querySelector("script"), null); |
| }, "streamAppendHTML on ShadowRoot uses safe sanitizer");</script> |