Skip to content

Commit

Permalink
Make XHR document.lastModified test handle summer to winter time change.
Browse files Browse the repository at this point in the history
document.lastModified is a legacy API which is tricky to test because it
reports times in the local timezone, but without any timezone indicator.
Such times cannot be reliably converted to UTC, because times during the
hour on either side of the change from summer to winter time produce the
same local time string as times on the other side of the change.

This change moves the code in document-lastModified-01.html that already
deals with this complexity into a shared support script and then uses it
to fix the equivalent test in responsexml-document-properties.htm, which
still has failures around the change from summer time to winter time, as
document-lastModified-01.html used to have.  This also renames the moved
function and constant to fit better in a separate support script.

Bug: 1382239
Change-Id: I3f45cdaae3908016235c715f8e7400b6eaf6492b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5006704
Reviewed-by: Mason Freed <[email protected]>
Commit-Queue: David Baron <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1220700}
  • Loading branch information
dbaron authored and chromium-wpt-export-bot committed Nov 7, 2023
1 parent f30784e commit 4bd41bb
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 87 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,101 +3,23 @@
<meta name="timeout" content="long">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="support/document-lastModified-utils.js"></script>
<div id="log"></div>
<script>
var last_modified = document.lastModified;

var pattern = /^([0-9]{2})\/([0-9]{2})\/([0-9]{4}) ([0-9]{2}):([0-9]{2}):([0-9]{2})$/;

test(function() {
assert_regexp_match(last_modified, pattern,
assert_regexp_match(last_modified, DOCUMENT_LASTMODIFIED_REGEX,
"Format should match the pattern \"NN/NN/NNNN NN:NN:NN\".");
}, "Date returned by lastModified is in the form \"MM/DD/YYYY hh:mm:ss\".");

function assert_date_string_approximately_now(str) {
// We want to test that |str| was a time in the user's local
// timezone generated within a few seconds prior to the present.
// This requires some care, since it is possible that:
// - the few second difference may have crossed a
// year/month/day/hour/minute boundary
// - the few second difference may have crossed a change in the
// local timezone's UTC offset
// - the local time might be one that has multiple valid UTC
// representations (for example, because it's in the hour
// following a shift from summer time to winter time)
// We will make some assumptions to do this:
// - local time's UTC offset doesn't change more than once per
// minute
// - local time's UTC offset only changes by integral numbers of
// minutes

// The date must be equal to or earlier than the present time.
var dmax = new Date();

// The date must be equal to or later than 2.5 seconds ago.
var TOLERANCE_MILLISECONDS = 2500;
var dmin = new Date();
dmin.setTime(dmax.getTime() - TOLERANCE_MILLISECONDS);

// Extract the year/month/date/hours/minutes/seconds from str. It
// is important that we do *not* try to construct a Date object from
// these, since the core of the date object is a timestamp in UTC,
// and there are cases (such as the hour on each side of a change
// from summer time to winter time) where there are multiple
// possible UTC timestamps for a given YYYY-MM-DD HH:MM:SS, and
// constructing a Date object would pick one of them, which might be
// the wrong one. However, we already have the right one in dmin
// and dmax, so we should instead extract local time from those
// rather than converting these values to UTC.
var m = pattern.exec(str);
var syear = Number(m[3]);
var smonth = Number(m[1]) - 1; // match Javascript 0-based months
var sdate = Number(m[2]);
var shours = Number(m[4]);
var sminutes = Number(m[5]);
var sseconds = Number(m[6]);

if (dmin.getFullYear() == dmax.getFullYear() &&
dmin.getMonth() == dmax.getMonth() &&
dmin.getDate() == dmax.getDate() &&
dmin.getHours() == dmax.getHours() &&
dmin.getMinutes() == dmax.getMinutes()) {
// min and max have the same minute
assert_equals(smonth, dmin.getMonth(), "month");
assert_equals(sdate, dmin.getDate(), "date");
assert_equals(syear, dmin.getFullYear(), "year");
assert_equals(shours, dmin.getHours(), "hours");
assert_equals(sminutes, dmin.getMinutes(), "minutes");
assert_true(dmin.getSeconds() <= sseconds &&
sseconds <= dmax.getSeconds(), "seconds");
} else if (dmin.getFullYear() == syear &&
dmin.getMonth() == smonth &&
dmin.getDate() == sdate &&
dmin.getHours() == shours &&
dmin.getMinutes() == sminutes) {
// actual value has the same minute as min
assert_true(dmin.getSeconds() <= sseconds, "dmin.getSeconds() <= sseconds");
assert_true(57 <= dmin.getSeconds(), "unexpected local time rules (dmin match)");
} else if (dmax.getFullYear() == syear &&
dmax.getMonth() == smonth &&
dmax.getDate() == sdate &&
dmax.getHours() == shours &&
dmax.getMinutes() == sminutes) {
// actual value has the same minute as max
assert_true(sseconds <= dmax.getSeconds(), "sseconds <= dmax.getSeconds()");
assert_true(dmax.getSeconds() <= 2, "unexpected local time rules (dmax match)");
} else {
assert_unreached("unexpected local time rules (no match)");
}
}

test(function() {
assert_date_string_approximately_now(last_modified);
assert_document_lastmodified_string_approximately_now(last_modified);
}, "Date returned by lastModified is current at page load");

var t = async_test("Date returned by lastModified is current after timeout.");
t.step_timeout(function() {
assert_date_string_approximately_now(document.lastModified);
assert_document_lastmodified_string_approximately_now(document.lastModified);
t.done();
}, 4000);
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
const DOCUMENT_LASTMODIFIED_REGEX = /^([0-9]{2})\/([0-9]{2})\/([0-9]{4}) ([0-9]{2}):([0-9]{2}):([0-9]{2})$/;

function assert_document_lastmodified_string_approximately_now(str) {
// We want to test that |str| was a time in the user's local
// timezone generated within a few seconds prior to the present.
// This requires some care, since it is possible that:
// - the few second difference may have crossed a
// year/month/day/hour/minute boundary
// - the few second difference may have crossed a change in the
// local timezone's UTC offset
// - the local time might be one that has multiple valid UTC
// representations (for example, because it's in the hour
// following a shift from summer time to winter time)
// We will make some assumptions to do this:
// - local time's UTC offset doesn't change more than once per
// minute
// - local time's UTC offset only changes by integral numbers of
// minutes

// The date must be equal to or earlier than the present time.
var dmax = new Date();

// The date must be equal to or later than 2.5 seconds ago.
var TOLERANCE_MILLISECONDS = 2500;
var dmin = new Date();
dmin.setTime(dmax.getTime() - TOLERANCE_MILLISECONDS);

// Extract the year/month/date/hours/minutes/seconds from str. It
// is important that we do *not* try to construct a Date object from
// these, since the core of the date object is a timestamp in UTC,
// and there are cases (such as the hour on each side of a change
// from summer time to winter time) where there are multiple
// possible UTC timestamps for a given YYYY-MM-DD HH:MM:SS, and
// constructing a Date object would pick one of them, which might be
// the wrong one. However, we already have the right one in dmin
// and dmax, so we should instead extract local time from those
// rather than converting these values to UTC.
var m = DOCUMENT_LASTMODIFIED_REGEX.exec(str);
var syear = Number(m[3]);
var smonth = Number(m[1]) - 1; // match Javascript 0-based months
var sdate = Number(m[2]);
var shours = Number(m[4]);
var sminutes = Number(m[5]);
var sseconds = Number(m[6]);

if (dmin.getFullYear() == dmax.getFullYear() &&
dmin.getMonth() == dmax.getMonth() &&
dmin.getDate() == dmax.getDate() &&
dmin.getHours() == dmax.getHours() &&
dmin.getMinutes() == dmax.getMinutes()) {
// min and max have the same minute
assert_equals(smonth, dmin.getMonth(), "month");
assert_equals(sdate, dmin.getDate(), "date");
assert_equals(syear, dmin.getFullYear(), "year");
assert_equals(shours, dmin.getHours(), "hours");
assert_equals(sminutes, dmin.getMinutes(), "minutes");
assert_true(dmin.getSeconds() <= sseconds &&
sseconds <= dmax.getSeconds(), "seconds");
} else if (dmin.getFullYear() == syear &&
dmin.getMonth() == smonth &&
dmin.getDate() == sdate &&
dmin.getHours() == shours &&
dmin.getMinutes() == sminutes) {
// actual value has the same minute as min
assert_true(dmin.getSeconds() <= sseconds, "dmin.getSeconds() <= sseconds");
assert_true(57 <= dmin.getSeconds(), "unexpected local time rules (dmin match)");
} else if (dmax.getFullYear() == syear &&
dmax.getMonth() == smonth &&
dmax.getDate() == sdate &&
dmax.getHours() == shours &&
dmax.getMinutes() == sminutes) {
// actual value has the same minute as max
assert_true(sseconds <= dmax.getSeconds(), "sseconds <= dmax.getSeconds()");
assert_true(dmax.getSeconds() <= 2, "unexpected local time rules (dmax match)");
} else {
assert_unreached("unexpected local time rules (no match)");
}
}
7 changes: 2 additions & 5 deletions xhr/responsexml-document-properties.htm
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
<title>XMLHttpRequest: responseXML document properties</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="../html/dom/documents/resource-metadata-management/support/document-lastModified-utils.js"></script>
</head>
<body>
<div id="log"></div>
<script>
var timePreXHR = Math.floor(new Date().getTime(new Date().getTime() - 3000) / 1000); // three seconds ago, in case there's clock drift
var client = new XMLHttpRequest()
client.open("GET", "resources/well-formed.xml", false)
client.send(null)
Expand Down Expand Up @@ -87,10 +87,7 @@
}, "Test document URL properties of document with <base> after redirect");

test(function() {
var lastModified = Math.floor(parseLastModified(client.responseXML.lastModified).getTime() / 1000);
var now = Math.floor(new Date().getTime(new Date().getTime() + 3000) / 1000); // three seconds from now, in case there's clock drift
assert_greater_than_equal(lastModified, timePreXHR);
assert_less_than_equal(lastModified, now);
assert_document_lastmodified_string_approximately_now(client.responseXML.lastModified);
}, 'lastModified set to time of response if no HTTP header provided')

test(function() {
Expand Down

0 comments on commit 4bd41bb

Please sign in to comment.