Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tests for the document promises #4171

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>document.parsed, document.contentLoaded, and document.loaded when document.open() happens inside DOMContentLoaded</title>
<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
<link rel="help" href="https://html.spec.whatwg.org/multipage/#dom-document-parsed">
<link rel="help" href="https://html.spec.whatwg.org/multipage/#dom-document-contentloaded">
<link rel="help" href="https://html.spec.whatwg.org/multipage/#dom-document-loaded">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>

<!-- This is testing /~https://github.com/whatwg/html/pull/1936#issuecomment-258952129 -->

<iframe src="document-promises-with-document-open-support.html"></iframe>

<script>
"use strict";

async_test(t => {
let contentDoc, parsedBefore, contentLoadedBefore, loadedBefore;
const fulfilled1 = [];
const fulfilled2 = [];

window.onload = t.step_func(() => {
contentDoc = frames[0].document;

parsedBefore = contentDoc.parsed;
contentLoadedBefore = contentDoc.contentLoaded;
loadedBefore = contentDoc.loaded;

contentDoc.parsed.then(() => {
fulfilled1.push("parsed");
});
contentDoc.contentLoaded.then(() => {
fulfilled1.push("contentLoaded");
});
contentDoc.loaded.then(() => {
fulfilled1.push("loaded");
});

contentDoc.addEventListener("DOMContentLoaded", t.step_func(() => {
assert_equals(contentDoc.parsed, parsedBefore,
"document.parsed must not have changed by the time a DOMContentLoaded handler fires");
assert_equals(contentDoc.contentLoaded, contentLoadedBefore,
"document.contentLoaded must not have changed by the time a DOMContentLoaded handler fires");
assert_equals(contentDoc.loaded, loadedBefore,
"document.loaded must not have changed by the time a DOMContentLoaded handler fires");

assert_array_equals(fulfilled1, ["parsed"],
"Only parsed must have been fulfilled by the time the DOMContentLoaded handler fires");

contentDoc.open();

assert_equals(contentDoc.readyState, "loading", "After document.open(), readyState must be loading");

assert_not_equals(contentDoc.parsed, parsedBefore, "document.open() must reset document.parsed");
assert_not_equals(contentDoc.contentLoaded, contentLoadedBefore,
"document.open() must reset document.contentLoaded");
assert_not_equals(contentDoc.loaded, loadedBefore, "document.open() must reset document.loaded");

contentDoc.parsed.then(() => {
fulfilled2.push("parsed");
});
contentDoc.contentLoaded.then(() => {
fulfilled2.push("contentLoaded");
});
contentDoc.loaded.then(() => {
fulfilled2.push("loaded");
});

t.step_timeout(() => {
assert_array_equals(fulfilled1, ["parsed", "contentLoaded"],
"Of the original promises, 10 ms after document.open(), parsed and contentLoaded must have fulfilled");
assert_array_equals(fulfilled2, [], "None of the new promises should be fulfilled 10 ms after document.open()");

contentDoc.close();
}, 10);
}));
});
});
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>Support document to load in an iframe</title>
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>document.parsed, document.contentLoaded, and document.loaded with document.open()</title>
<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
<link rel="help" href="https://html.spec.whatwg.org/multipage/#dom-document-parsed">
<link rel="help" href="https://html.spec.whatwg.org/multipage/#dom-document-contentloaded">
<link rel="help" href="https://html.spec.whatwg.org/multipage/#dom-document-loaded">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>

<iframe src="document-promises-with-document-open-support.html"></iframe>

<script>
"use strict";

const loadPromise = new Promise(resolve => window.onload = resolve);

function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

promise_test(() => {
let contentDoc, parsedBefore, contentLoadedBefore, loadedBefore;
const fulfilled = [];

return loadPromise.then(() => {
contentDoc = frames[0].document;

parsedBefore = contentDoc.parsed;
contentLoadedBefore = contentDoc.contentLoaded;
loadedBefore = contentDoc.loaded;

// Sanity check: they all work
return Promise.all([parsedBefore, contentLoadedBefore, loadedBefore]);
})
.then(() => {
contentDoc.open();

assert_equals(contentDoc.readyState, "loading", "After document.open(), readyState must be loading");

assert_not_equals(contentDoc.parsed, parsedBefore, "document.open() must reset document.parsed");
assert_not_equals(contentDoc.contentLoaded, contentLoadedBefore,
"document.open() must reset document.contentLoaded");
assert_not_equals(contentDoc.loaded, loadedBefore, "document.open() must reset document.loaded");

contentDoc.parsed.then(() => {
fulfilled.push("parsed");
});
contentDoc.contentLoaded.then(() => {
fulfilled.push("contentLoaded");
});
contentDoc.loaded.then(() => {
fulfilled.push("loaded");
});

return delay(10);
})
.then(() => {
assert_array_equals(fulfilled, [], "None of the promises should be fulfilled 10 ms after document.open()");

contentDoc.close();

return Promise.all([contentDoc.parsed, contentDoc.contentLoaded, contentDoc.loaded]);
});
});
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>document.parsed, document.contentLoaded, and document.loaded</title>
<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
<link rel="help" href="https://html.spec.whatwg.org/multipage/#dom-document-parsed">
<link rel="help" href="https://html.spec.whatwg.org/multipage/#dom-document-contentloaded">
<link rel="help" href="https://html.spec.whatwg.org/multipage/#dom-document-loaded">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>

<script>
"use strict";

let onDomContentLoadedFired = false;
let onLoadFired = false;
const readyStateChanges = [];

let parsedFulfilled = false;
let contentLoadedFulfilled = false;
let loadedFulfilled = false;

let parsedAssertions, contentLoadedAssertions, loadedAssertions;

test(() => {
document.addEventListener("readystatechange", () => {
readyStateChanges.push(document.readyState);
});

document.addEventListener("DOMContentLoaded", () => {
onDomContentLoadedFired = true;
});

window.addEventListener("load", () => {
onLoadFired = true;
});

// A note on timing:
// The readystatechange event handler function must execute before the promise fulfillment callback does, because the
// sequence goes:
// - UA code resolves the promise, thus enqueuing a microtask to call the onFulfilled
// - UA code fires an event
// - Eventually this uses Web IDL to invoke the callback
// (https://heycam.github.io/webidl/#call-a-user-objects-operation)
// - *After* the callback runs, we perform a microtask checkpoint
// - The microtask checkpoint calls onFulfilled

parsedAssertions = document.parsed.then(value => {
parsedFulfilled = true;

assert_equals(value, undefined, "The document.parsed promise must fulfill with undefined");
assert_array_equals(readyStateChanges, ["parsed"],
"Inside the document.parsed fulfillment handler, the readystatechange event must have fired once already");
assert_equals(document.readyState, "interactive",
"Inside the document.parsed fulfillment handler, readyState must be interactive");
});

contentLoadedAssertions = document.contentLoaded.then(value => {
contentLoadedFulfilled = true;

assert_equals(value, undefined, "The document.contentLoaded promise must fulfill with undefined");
assert_equals(document.readyState, "interactive",
"Inside the document.contentLoaded fulfillment handler, readyState must be interactive");
assert_equals(parsedFulfilled, true,
"Inside the document.contentLoaded fulfillment handler, document.parsed must have already fulfilled");
assert_equals(onDomContentLoadedFired, true,
"Inside the document.contentLoaded fulfillment handler, DOMContentLoaded must have already fired");
assert_array_equals(readyStateChanges, ["interactive"],
"Inside the document.contentLoaded fulfillment handler, the readystatechange event must have fired exactly once");
});

loadedAssertions = document.loaded.then(value => {
loadedFulfilled = true;

assert_equals(value, undefined, "The document.loaded promise must fulfill with undefined");
assert_equals(document.readyState, "complete",
"Inside the document.loaded fulfillment handler, readyState must be complete");
assert_equals(parsedFulfilled, true,
"Inside the document.loaded fulfillment handler, document.parsed must have already fulfilled");
assert_equals(contentLoadedFulfilled, true,
"Inside the document.loaded fulfillment handler, document.contentLoaded must have already fulfilled");
assert_equals(onDomContentLoadedFired, true,
"Inside the document.loaded fulfillment handler, DOMContentLoaded must have already fired");
assert_equals(onLoadFired, false,
"Inside the document.loaded fulfillment handler, load must not have already fired");
assert_array_equals(readyStateChanges, ["interactive", "complete"],
"Inside the document.loaded fulfillment handler, the readystatechange event must have fired exactly twice");
});
}, "Setup code");

promise_test(() => parsedAssertions || assert_unreached(), "document.parsed");
promise_test(() => contentLoadedAssertions || assert_unreached(), "document.contentLoaded");
promise_test(() => loadedAssertions || assert_unreached(), "document.loaded");

// Historical names must not be supported
test(() => {
assert_false('interactive' in document);
}, 'document.interactive must not be supported')
</script>
9 changes: 6 additions & 3 deletions html/dom/interfaces.html
Original file line number Diff line number Diff line change
Expand Up @@ -915,14 +915,17 @@ <h1>HTML IDL tests</h1>
typedef (HTMLScriptElement or SVGScriptElement) HTMLOrSVGScriptElement;

[OverrideBuiltins]
partial /*sealed*/ interface Document {
partial interface Document {
// resource metadata management
[PutForwards=href, Unforgeable] readonly attribute Location? location;
attribute USVString domain;
attribute USVString domain;
readonly attribute USVString referrer;
attribute USVString cookie;
attribute USVString cookie;
readonly attribute DOMString lastModified;
readonly attribute DocumentReadyState readyState;
[Unscopable] readonly attribute Promise<void> parsed;
[Unscopable] readonly attribute Promise<void> contentLoaded;
[Unscopable] readonly attribute Promise<void> loaded;

// DOM tree accessors
getter object (DOMString name);
Expand Down