A native date field can display the intended day, expose the same day through its string value, and still lose that day when application code crosses through Date. The defect can happen in either direction. On a read, valueAsDate represents the selected day at UTC midnight, so local calendar getters in a negative-offset zone can report yesterday. On a write, a Date constructed at local midnight in a positive-offset zone can belong to the previous UTC day, so assigning it through valueAsDate can rewrite the control to yesterday.
That is not evidence that the browser randomly changes dates. It is evidence that the application mixed two representations: a date-only string and an instant-bearing Date. The repair is therefore not a universal +1 day, a fixed-hour adjustment, or a new date library. It is a declared boundary: preserve the normalized string when the business value is date-only, or use UTC fields consistently when an existing interface requires a Date.
This playbook demonstrates that boundary with one ordinary <input type="date">, eight DOM checks, and three Chromium contexts: UTC, America/Los_Angeles, and Asia/Tokyo. The resulting 24-row ledger includes a leap-day read counterexample, a positive-offset setter counterexample, a year-boundary control, empty and invalid states, and two safe round trips. The final decisions are deliberately scoped: ACCEPT_BOUNDED, REPAIR, or HOLD.
Declare the date-only value the form must preserve
The field under test represents a calendar date: 2024-03-01 or 2025-01-01. It does not represent a meeting at midnight, an event timestamp, a viewer-local instant, or a server-local instant. The contract to preserve is the same year, month, and day that entered the component.
For this article, the canonical application value is the normalized input string:
YYYY-MM-DDThe two literals are independent oracles. Expected dates are not calculated from whichever output the browser happens to return. For the date-preserving paths, March 1 must remain 2024-03-01, and UTC reads of January 1 must remain 2025-01-01. The negative controls deliberately expect specified, wrong-for-the-application dates. That distinction matters because an output-derived oracle can approve the same defect it was supposed to detect.
MDN’s date-input reference documents the normalized date string and distinguishes it from the localized user interface. For the four-digit years used here, the nonempty value has the form yyyy-mm-dd. The stored value and visual formatting are therefore separate concerns; this experiment asserts the former and makes no claim about a particular picker rendering.
Readers who need the markup and form-element basics can use HTML form foundations as background. Here, the control is already present, enabled, and addressable. The engineering question is narrower: does the JavaScript representation boundary preserve the literal day?
Read the UTC contract of valueAsDate
For the date state, the HTML Living Standard defines both conversion directions in UTC. Converting a valid input string to a Date produces midnight UTC on the parsed date. Converting a valid Date back to the input string selects the calendar date current at that instant in UTC. The maintained standard consulted on October 14, 2026 states both algorithms explicitly; that consultation date is not a feature launch date.
MDN’s HTMLInputElement.valueAsDate reference, last modified April 10, 2025 and consulted on October 14, 2026, gives the corresponding warning: interpret the returned object as UTC and prefer UTC accessors. Local interpretation in a negative offset can report the previous day. It also documents a Date or null result when conversion is impossible.
MDN’s later formatting example uses local presentation and explicitly warns that its result may be a day off. That example is not a date-only serialization recipe. Its warning is consistent with the UTC definition above, so copy the representation contract rather than assuming every display example preserves the selected day.
That establishes four surfaces to distinguish:
Surface | Example | Contract used here |
input.value | "2024-03-01" | Normalized date-only string |
input.valueAsDate on read | Date at 2024-03-01T00:00:00.000Z | UTC bridge |
Local getters on that Date | Zone-dependent fields | Unsafe for this bridge |
input.valueAsDate = someDate | String chosen from someDate’s UTC fields | UTC setter conversion |
This is a native API contract, not a request to undertake a separate Temporal migration workflow. The application can retain Date; it must simply stop combining the input’s UTC bridge with local calendar operations.
A calendar string and a Date are different representations
The string 2024-03-01 names a calendar day without choosing an hour or zone. The Date returned by valueAsDate carries one exact millisecond value, and the input algorithm chooses UTC midnight to create that bridge. That choice does not mean the user selected a midnight appointment. It does not add a business timezone to the field. It only specifies how the browser maps between the date state and a Date object.
Once code obtains that object, every accessor must be judged against the bridge. getUTCFullYear(), getUTCMonth(), and getUTCDate() view the fields the input encoded. getFullYear(), getMonth(), and getDate() view the same instant in the browser context’s local timezone. MDN’s Date component reference distinguishes these accessor families. Using the local family here changes the question from “which date did the input encode?” to “which local calendar date contains this UTC instant?”
Preserve null as well. An empty control has no date to reinterpret, and manufacturing today, the Unix epoch, or another fallback would create data rather than decode it.
Run the full three-timezone browser fixture
This Playwright timezone date input fixture runs the same eight checks in three browser contexts. Playwright’s emulation documentation exposes locale and timezone_id as browser-context configuration. The test holds the HTML and JavaScript constant while changing the effective browser timezone. That supports the setup mechanism; it does not establish equivalence across every browser engine or operating system.
Use an isolated Python environment and install playwright==1.57.0. Save the complete driver as html_date_fixture.py. These shell commands assume a POSIX-style shell and an available Python installation. Set CHROMIUM_EXECUTABLE to an actual installed Chromium executable, replacing the example path. Without that variable, the script first searches PATH for chromium; only when that lookup is empty does it use Playwright’s managed default. Merely installing a managed browser does not override an existing system lookup.
python -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install playwright==1.57.0
# Replace this path with the installed browser being qualified:
export CHROMIUM_EXECUTABLE=/path/to/chromium
# Managed fallback only when no system chromium is found on PATH:
# unset CHROMIUM_EXECUTABLE
# python -m playwright install chromium
python html_date_fixture.pyPlaywright’s browser installation guidance ties browser binaries to package versions. Its browser launch documentation cautions that arbitrary external executables are not guaranteed compatible. The recorded system-browser combination is therefore a bounded observation, not a general compatibility promise. A managed-browser run must record its actual browser version too.
The script prints the generated results.json path. Retain that report with the exact source used to create it. A useful piece of browser regression evidence is not merely a green total; it includes the environment, literal expectations, row-level observations, and source identity.
The known-answer manifest is intentionally asymmetric:
ID | Operation | Required oracle |
D1 | Read 2024-03-01 through valueAsDate | String, UTC fields, and ISO all identify March 1 |
D2 | Read that UTC Date through local fields | Zone-specific local date; Los Angeles exposes February 29 |
D3 | Assign new Date(2024, 2, 1) | Zone-specific setter result; Tokyo exposes February 29 |
D4 | Assign new Date(Date.UTC(2024, 2, 1)) | March 1 in every context |
D5 | Save and restore input.value | March 1 in every context |
D6 | Read 2025-01-01 through UTC and local fields | UTC remains January 1; Los Angeles local fields expose December 31 |
D7 | Read an empty control | Empty string and valueAsDate === null |
D8 | Assign an invalid Date | Empty string and valueAsDate === null on readback |
"""Programmatic HTML date API checks in three emulated Chromium timezones."""
import hashlib
import importlib.metadata
import json
import os
import platform
import shutil
import tempfile
from datetime import datetime, timezone
from pathlib import Path
from playwright.sync_api import sync_playwright
# Independent calendar oracles for the chosen dates, not output-derived corrections.
ZONES = {
"UTC": {"local_march": "2024-03-01", "local_midnight_set": "2024-03-01", "local_january": "2025-01-01"},
"America/Los_Angeles": {"local_march": "2024-02-29", "local_midnight_set": "2024-03-01", "local_january": "2024-12-31"},
"Asia/Tokyo": {"local_march": "2024-03-01", "local_midnight_set": "2024-02-29", "local_january": "2025-01-01"},
}
JAVASCRIPT = r"""() => {
const d = document.querySelector('#date');
const pad = n => String(n).padStart(2, '0');
const utc = x => ${x.getUTCFullYear()}-${pad(x.getUTCMonth()+1)}-${pad(x.getUTCDate())};
const local = x => ${x.getFullYear()}-${pad(x.getMonth()+1)}-${pad(x.getDate())};
const rows = [];
d.value = '2024-03-01';
rows.push({id:'D1', value:d.value, utc:utc(d.valueAsDate), iso:d.valueAsDate.toISOString()});
rows.push({id:'D2', value:d.value, local:local(d.valueAsDate), offset_minutes:d.valueAsDate.getTimezoneOffset()});
const localMidnight = new Date(2024, 2, 1);
d.valueAsDate = localMidnight;
rows.push({id:'D3', value:d.value, supplied_iso:localMidnight.toISOString()});
d.valueAsDate = new Date(Date.UTC(2024, 2, 1));
rows.push({id:'D4', value:d.value});
d.value = '2024-03-01';
const saved = d.value; d.value = ''; d.value = saved;
rows.push({id:'D5', value:d.value});
d.value = '2025-01-01';
rows.push({id:'D6', value:d.value, utc:utc(d.valueAsDate), local:local(d.valueAsDate)});
d.value = '';
rows.push({id:'D7', value:d.value, date_is_null:d.valueAsDate === null});
d.value = '2024-03-01'; d.valueAsDate = new Date(NaN);
rows.push({id:'D8', value:d.value, date_is_null:d.valueAsDate === null});
return {timezone:Intl.DateTimeFormat().resolvedOptions().timeZone, input_type:d.type, rows};
}"""
def require(ok, message):
if not ok:
raise RuntimeError(message)
def main():
source = Path(__file__).resolve()
directory = Path(tempfile.mkdtemp(prefix="html-date-", dir=source.parent))
report = {"fixture": "html-date-v1", "status": "FAIL", "cases": [],
"started_utc": datetime.now(timezone.utc).isoformat(),
"python": platform.python_version(), "platform": platform.platform(),
"playwright": importlib.metadata.version("playwright"),
"source_sha256": hashlib.sha256(source.read_bytes()).hexdigest()}
try:
with sync_playwright() as engine:
executable = os.environ.get("CHROMIUM_EXECUTABLE") or shutil.which("chromium")
launch = {"headless": True}
if executable:
launch["executable_path"] = executable
with engine.chromium.launch(**launch) as browser:
report["browser"] = browser.version
report["executable"] = executable or "Playwright-managed Chromium"
for zone, oracle in ZONES.items():
context = browser.new_context(timezone_id=zone, locale="en-US")
try:
page = context.new_page()
page.set_content('<!doctype html><label>Date <input id="date" type="date"></label>')
observed = page.evaluate(JAVASCRIPT)
require(observed["input_type"] == "date", "Native date state unavailable")
require(observed["timezone"] == zone, "Unexpected timezone emulation")
expected = {
"D1": {"value": "2024-03-01", "utc": "2024-03-01", "iso": "2024-03-01T00:00:00.000Z"},
"D2": {"value": "2024-03-01", "local": oracle["local_march"]},
"D3": {"value": oracle["local_midnight_set"]},
"D4": {"value": "2024-03-01"},
"D5": {"value": "2024-03-01"},
"D6": {"value": "2025-01-01", "utc": "2025-01-01", "local": oracle["local_january"]},
"D7": {"value": "", "date_is_null": True},
"D8": {"value": "", "date_is_null": True},
}
require([r["id"] for r in observed["rows"]] == list(expected), "Incomplete DOM ledger")
for row in observed["rows"]:
row.update(zone=zone, status="FAIL")
report["cases"].append(row)
for key, value in expected[row["id"]].items():
require(row[key] == value, f'{zone}/{row["id"]}: wrong {key}')
row["status"] = "PASS"
finally:
context.close()
require(len(report["cases"]) == 24, "Incomplete timezone matrix")
report["status"] = "PASS_24_CASES"
except Exception as error:
report["error"] = {"type": type(error).__name__, "message": str(error)}
raise
finally:
report["finished_utc"] = datetime.now(timezone.utc).isoformat()
path = directory / "results.json"
path.write_text(json.dumps(report, indent=2, allow_nan=False) + "\n", encoding="utf-8")
print(path)
if name == "__main__":
main()The commissioning record supplied with this fixture reports PASS_24_CASES, starting at 2026-10-14T06:42:50.556592+00:00 and finishing at 2026-10-14T06:42:51.620725+00:00. It identifies Python 3.13.5 on Linux-6.18.44-x86_64-with-glibc2.41, Playwright 1.57.0, and Chromium 144.0.7559.96. Its source SHA-256 is 62fa0106e4eebd2d8145a1dfb2921bf2e8b4e60ea64e43471ca39adc5421f359. These timestamps identify the reported execution, not a benchmark. The displayed driver reproduces that digest when saved with its original spaces, LF line endings, and a final newline. Matching bytes identify source; they do not independently authenticate an earlier execution or establish browser behavior.
Verify the context before trusting the date rows
The driver evaluates the JavaScript first, then checks that the returned input_type is exactly date and the resolved timezone matches the requested zone before marking any row as passed. These are acceptance guards, not checks that run before DOM evaluation. An unsupported implementation could fail during evaluation before either guard is reached. Playwright’s context emulation controls establish the requested setup, while the returned values and assertions determine whether this run is usable.
The report records browser version, executable selection, Playwright package version, Python version, platform, timestamps, and source hash. Those fields distinguish the commissioning record from a later rerun. Changes to comments, whitespace, assertions, or line endings produce different source bytes and ordinarily a different digest. The driver does not retain input_type or the resolved timezone as separate report fields; it checks them, then records the requested zone on each row. If evaluation fails early, do not interpret a missing or partial case list as completed coverage.
Read all 24 rows. PASS_24_CASES means the checked fields of each row matched its declared oracle. It does not mean every exercised path is acceptable application behavior. D2 in Los Angeles and D3 in Tokyo are negative controls: they pass because they expose the known representation defects. D6 in Los Angeles confirms the read defect at a year boundary. The additional offset_minutes and supplied_iso fields are logged evidence, not directly asserted by this driver. Inspect them separately instead of treating the aggregate status as an assertion over every field.
Observe the UTC midnight returned by the control
D1 assigns the literal string 2024-03-01 to the control. In every context, the observed row contains:
{
"value": "2024-03-01",
"utc": "2024-03-01",
"iso": "2024-03-01T00:00:00.000Z"
}That is the central HTML valueAsDate UTC observation. The normalized string remains March 1, and the Date bridge represents midnight UTC on March 1. The zone changes how that instant can be viewed locally; it does not change the getter’s required construction. The HTML date-state conversion algorithm specifies this UTC representation.
The ISO string is useful evidence because it makes the instant unambiguous. It is not the proposed business payload. Sending 2024-03-01T00:00:00.000Z where the interface promises a date-only value would change the contract from “calendar date” to “timestamp chosen as a bridge.” Another consumer could then localize that timestamp and display February 29.
For a date-only API, compare the intended string with the serialized string. For an adapter that genuinely requires Date, compare the full UTC year, month, and day. Do not decide correctness from a localized console rendering, because that asks a different question from the input contract.
Reproduce yesterday through local calendar getters
D2 reads a Date representing the same instant as D1, then formats it with getFullYear(), getMonth(), and getDate(). The getter is called again; the code does not reuse D1’s object. In UTC, local fields remain 2024-03-01. In Tokyo, the instant is 09:00 on March 1, so the local date also remains March 1. The Los Angeles row is:
{
"value": "2024-03-01",
"local": "2024-02-29",
"offset_minutes": 480
}The input did not lose its value: value is still 2024-03-01. The defect appears when application code asks local getters to interpret a Date that this input created at UTC midnight. MDN’s UTC interpretation warning explicitly identifies the previous-day risk in a negative offset.
This valueAsDate and getDate() off-by-one failure does not change the input string. A UTC-only test misses it, and Tokyo’s matching local date cannot approve the implementation. Los Angeles supplies the counterexample that rejects local getters for this contract.
Preserve offset_minutes in the report. It connects local calendar output to the effective context for that instant. Here the recorded offsets are 0, 480, and -540 minutes for UTC, Los Angeles, and Tokyo respectively. MDN’s Date timezone-offset explanation notes that an offset depends on the represented instant as well as the host timezone. Do not substitute the tester’s present offset or extrapolate one value to every date in a region.
Keep calendar boundaries in the oracle
March 1, 2024 is valuable because the wrong local reading crosses both a month boundary and a leap-day boundary: the observed date becomes February 29, not merely “day 0” in an abstract calculation. D6 supplies a second boundary. Reading 2025-01-01T00:00:00.000Z through Los Angeles local fields yields 2024-12-31, crossing the year as well as the day.
These literals make the defect unmistakable, but they are not exhaustive coverage of all calendar rules or timezone transitions. The fixture does not claim daylight-saving coverage, historical timezone-database validation, or correctness for every supported date. It qualifies two independent date literals and one documented conversion boundary.
Make local-midnight assignment fail in the opposite direction
D3 tests the setter rather than the getter. It constructs new Date(2024, 2, 1) and assigns that object to input.valueAsDate. The Date constructor documentation specifies that component arguments use local time; the zero-based month 2 selects March, and the omitted clock components default to zero. The supplied_iso field records the resulting instant.
The observed supplied instants are:
Context | Local-midnight Date as ISO | Setter output |
UTC | 2024-03-01T00:00:00.000Z | 2024-03-01 |
America/Los_Angeles | 2024-03-01T08:00:00.000Z | 2024-03-01 |
Asia/Tokyo | 2024-02-29T15:00:00.000Z | 2024-02-29 |
The HTML Date-to-string conversion uses the calendar date current at the supplied instant in UTC. Tokyo local midnight occurs while UTC is still on February 29, so the setter writes 2024-02-29. This positive-offset local-midnight setter defect is the opposite direction from Los Angeles’s read defect. The browser follows its conversion contract; the application supplied a representation that does not preserve the intended day.
Los Angeles exposes the read defect in D2 but not this write defect in D3. Tokyo exposes this write defect but not the D2 read defect. Pairing opposite-offset contexts rules out a one-sided workaround such as “always add one day when reading” or “only test west-of-UTC users.” The application must choose one representation boundary and apply it symmetrically.
Use a UTC bridge only when a Date is required
D4 constructs the setter value with Date.UTC(2024, 2, 1) and wraps the returned millisecond value in Date. MDN’s Date.UTC documentation specifies UTC component interpretation. All three contexts write 2024-03-01 because the object’s UTC calendar fields are deliberately March 1. Its local view may differ, but the input setter does not use that view.
The matching read rule is to extract all three UTC fields from valueAsDate:
function readDateOnlyThroughUtcBridge(input) {
const date = input.valueAsDate;
if (date === null) return null;
const pad = (number) => String(number).padStart(2, "0");
return ${date.getUTCFullYear()}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())};
}
function writeDateOnlyThroughUtcBridge(input, year, month, day) {
input.valueAsDate = new Date(Date.UTC(year, month - 1, day));
}These helpers illustrate the bounded 2024–2025 use case; they are not general date validators. MDN’s getUTCDate reference defines the UTC day-of-month accessor. The repair must also use matching UTC year and month accessors. Replacing only getDate() while leaving getFullYear() or getMonth() local still creates a mixed calendar.
The write helper assumes validated integer components in the declared range. Date.UTC() can normalize out-of-range components, so successful construction is not date validation. After writing, compare input.value with the independently approved string. The eight-case driver checks the underlying UTC operations, not every input combination to these illustrative helpers.
Use this bridge only where a Date-valued interface is genuinely required. The simpler string contract in D5 has fewer conversion points and is easier to audit.
Do not repair a date by adding a fixed number of hours
A fixed-hour patch encodes an offset assumption rather than the field’s meaning. Adding eight hours could conceal the Los Angeles D2 symptom for this literal, while doing nothing principled for Tokyo’s D3 setter direction. Adding 24 hours changes the represented instant and can convert one wrong boundary into another. Changing the machine timezone similarly alters the environment instead of repairing the component contract.
Retain the normalized string, or use UTC calendar fields at both ends of the required bridge. A fixed offset adjustment does not establish preservation and is unnecessary for this date-only payload.
Keep date-only storage as a date-only string
D5 performs the smallest round trip in the fixture:
d.value = "2024-03-01";
const saved = d.value;
d.value = "";
d.value = saved;Every context finishes with value === "2024-03-01". No Date is created, no timezone is consulted, and no calendar field is reinterpreted. For the declared payload contract, this is the preferred JavaScript date-only form value path.
A component boundary can make absence explicit while preserving the input date value string. This small illustration assumes a supported date control and a caller that supplies an already approved date string or the declared absence value:
function readDateOnly(input) {
return input.value === "" ? null : input.value;
}
function writeDateOnly(input, value) {
input.value = value ?? "";
}
const payload = {
effectiveDate: readDateOnly(document.querySelector("#date")),
};Document the accepted write types rather than using this helper as a permissive parser. Its nullish fallback clears an absent value; it cannot distinguish intentional absence from an upstream failure that the caller discarded. Keep that error information outside the native value and preserve it until the application decides whether sending an empty field is allowed.
For this illustrative payload, the intended JSON value is {"effectiveDate":"2024-03-01"}. D5 itself checks a DOM string round trip; it does not execute this payload helper, serialize JSON, or send a request. Do not call toISOString() merely because JSON is involved, because that creates a time-bearing representation. Avoid parsing a localized label or truncating an arbitrary Date.toString() result. MDN’s distinction between display and value identifies the normalized string this interface should preserve.
This laboratory deliberately does not construct FormData. The neighboring FormData field-selection validation addresses which controls become submission entries. Here, one ordinary enabled control is already selected; the date-only serialization validation asks whether its calendar string survives application reads and writes unchanged.
A date-only string is not universally the right representation. An appointment or event timestamp needs a separate contract. But a birthday, reporting day, effective date, or similar field should not be converted into an instant merely to pass through component state.
Validate the year boundary and empty states
D6 repeats the read check with 2025-01-01. UTC fields remain 2025-01-01 in all three contexts. Local fields are 2025-01-01 in UTC and Tokyo, but 2024-12-31 in Los Angeles. That independent literal proves the negative-offset read defect is not limited to the leap-day example. It can cross a year boundary while the stored input string remains correct.
D7 clears the control and checks an empty value with valueAsDate === null. D8 starts with 2024-03-01, assigns new Date(NaN), and checks the same empty-string and null readback. The HTML valueAsDate setter contract and MDN’s invalid-Date behavior also specify that assigning null clears the value. Direct assignment of null is documented behavior, not a separate executed case in this eight-case driver.
D8 does not preserve the old date or return an invalid Date object. It clears the value; the subsequent getter returns null. Keep that clearing behavior visible to the caller.
Those native outcomes are not a complete application policy. The component still has to decide whether absence is allowed, must be reported to the user, or prevents a request. The fixture only proves that empty and invalid setter states are observable without inventing a replacement date.
Preserve absence without inventing a replacement date
At the application level, distinguish a valid normalized date string, deliberate absence, and an upstream error that prevented a trustworthy date from being produced. The native empty-string/null state alone does not retain that history; the adapter must carry any required error information separately. Do not collapse these states into “today,” and do not introduce an epoch fallback such as new Date(0) that creates an unrelated calendar value.
Required-field policy remains separate from decoding. A null read signals absence, not permission to fabricate a selected date.
Reconcile every case across all three contexts
The commissioning record reports 24 passing fixture rows. The table shows the control’s value at each checkpoint alongside conversion evidence; D3 also states its intended March 1 target. “PASS” means the asserted fields matched their diagnostic oracle, not that the application preserved the intended date on every path.
Zone | ID | value | UTC, local, setter, or absence evidence | Fixture | Interpretation |
UTC | D1 | 2024-03-01 | UTC 2024-03-01; ISO 2024-03-01T00:00:00.000Z | PASS | Expected UTC getter bridge |
UTC | D2 | 2024-03-01 | Local 2024-03-01; offset 0 | PASS | Comparison; local fields happen to agree |
UTC | D3 | 2024-03-01 | Target 2024-03-01; Supplied ISO 2024-03-01T00:00:00.000Z | PASS | Local-midnight setter happens to agree |
UTC | D4 | 2024-03-01 | UTC-constructed setter value | PASS | Safe UTC bridge |
UTC | D5 | 2024-03-01 | String saved and restored | PASS | Preferred date-only path |
UTC | D6 | 2025-01-01 | UTC 2025-01-01; local 2025-01-01 | PASS | Boundary comparison |
UTC | D7 | "" | date_is_null=true | PASS | Expected absence |
UTC | D8 | "" | Invalid setter; date_is_null=true | PASS | Expected clearing behavior |
America/Los_Angeles | D1 | 2024-03-01 | UTC 2024-03-01; ISO 2024-03-01T00:00:00.000Z | PASS | Expected UTC getter bridge |
America/Los_Angeles | D2 | 2024-03-01 | Local 2024-02-29; offset 480 | PASS | Negative control: local-read contract failure |
America/Los_Angeles | D3 | 2024-03-01 | Target 2024-03-01; Supplied ISO 2024-03-01T08:00:00.000Z | PASS | No shift for this setter case |
America/Los_Angeles | D4 | 2024-03-01 | UTC-constructed setter value | PASS | Safe UTC bridge |
America/Los_Angeles | D5 | 2024-03-01 | String saved and restored | PASS | Preferred date-only path |
America/Los_Angeles | D6 | 2025-01-01 | UTC 2025-01-01; local 2024-12-31 | PASS | Year-boundary confirmation of read defect |
America/Los_Angeles | D7 | "" | date_is_null=true | PASS | Expected absence |
America/Los_Angeles | D8 | "" | Invalid setter; date_is_null=true | PASS | Expected clearing behavior |
Asia/Tokyo | D1 | 2024-03-01 | UTC 2024-03-01; ISO 2024-03-01T00:00:00.000Z | PASS | Expected UTC getter bridge |
Asia/Tokyo | D2 | 2024-03-01 | Local 2024-03-01; offset -540 | PASS | Comparison; local fields happen to agree |
Asia/Tokyo | D3 | 2024-02-29 | Target 2024-03-01; Supplied ISO 2024-02-29T15:00:00.000Z | PASS | Negative control: local-midnight setter failure |
Asia/Tokyo | D4 | 2024-03-01 | UTC-constructed setter value | PASS | Safe UTC bridge |
Asia/Tokyo | D5 | 2024-03-01 | String saved and restored | PASS | Preferred date-only path |
Asia/Tokyo | D6 | 2025-01-01 | UTC 2025-01-01; local 2025-01-01 | PASS | Boundary comparison |
Asia/Tokyo | D7 | "" | date_is_null=true | PASS | Expected absence |
Asia/Tokyo | D8 | "" | Invalid setter; date_is_null=true | PASS | Expected clearing behavior |
The matrix rules out two misleading summaries. First, “valueAsDate always returns the wrong day” is false: its UTC representation matches the documented date input contract. The wrong read comes from applying local getters to that UTC bridge. Second, “local midnight is always safe to assign” is false: Tokyo’s local-midnight instant belongs to February 29 in UTC, and the setter correctly uses that UTC date.
The date input timezone round trip is therefore operation-specific. Preserve the string, or keep the bridge consistently UTC. Do not infer a correction from the zone in which a defect happened to be visible.
Keep browser and integration claims within the evidence
The identified commissioning run concerns programmatic native DOM APIs in headless Chromium 144.0.7559.96, controlled by Playwright 1.57.0 under Python 3.13.5. It uses a fresh page containing a local HTML fragment and no application endpoint. The locale is fixed to en-US; timezone changes per context. Playwright’s emulation documentation supports this setup, but it does not turn one Chromium run into evidence for every engine.
The driver does not test manual picker interaction, mobile keyboards, visual localization, screen readers, framework wrappers, state libraries, request serialization, server parsing, Firefox, Safari, or embedded webviews. It also does not establish the result for every Chromium build or timezone database. The specification explains the API contract; the supplied commissioning record identifies one reported observation set in one environment. Reproducing the driver produces new evidence with its own timestamps, not a replacement identity for that earlier run.
A future component or integration qualification should create its own report and add only the layers it actually exercises. A practical unexecuted protocol would:
Render the production component around the native control and assert both its visible state and underlying value.
Capture the exact adapter output before any request, distinguishing YYYY-MM-DD, Date, and timestamp representations.
Inspect the serialized request body and the response value without relying on console formatting.
Repeat the literal March 1 and January 1 oracles in every supported browser engine and wrapper version.
Add manual picker and accessibility testing as separate evidence, because programmatic assignment does not cover interaction behavior.
Do not report that protocol as completed until it has an actual environment, timestamps, artifacts, and row-level results. A syntax check proves only that the driver parses. A local DOM pass proves neither remote integration nor untested browser behavior.
Choose the acceptance or repair action
The aggregate fixture result is green, but the engineering decision is made per interface:
Interface or evidence state | Decision | Required action |
Date-only string retained as input.value and serialized unchanged | ACCEPT_BOUNDED | Keep exact string and null policy; regression-test literal round trips |
Date bridge read and written with matching UTC year, month, and day | ACCEPT_BOUNDED | Centralize adapter; retain D1, D4, D6, D7, and D8 checks |
valueAsDate followed by local calendar getters | REPAIR | Replace the mixed read with the string contract or all-UTC accessors |
Local-midnight Date assigned through valueAsDate | REPAIR | Preserve the string or construct the required UTC calendar date |
Stored date with no trustworthy original or conversion provenance | HOLD | Do not apply a blanket day correction |
Browser, wrapper, or integration behavior outside executed evidence | HOLD | Obtain scoped execution evidence before approval |
ACCEPT_BOUNDED is not a statement that date handling everywhere is correct. It accepts only the declared representation and the tested operations. REPAIR applies where the code mixes UTC and local calendar semantics even if a UTC developer machine makes the test appear green. HOLD protects records and platforms whose intended result cannot be established from current evidence.
The preferred repair order is: preserve input.value; otherwise centralize a UTC-only bridge; then remove direct local getter or local-midnight setter calls from component code. Keep the two negative controls so a future refactor cannot make the tests green by deleting the counterexamples.
Keep field validity separate from semantic preservation
A syntactically valid YYYY-MM-DD value can still name the wrong business day. Tokyo D3 produces the valid native value 2024-02-29, but it fails the intended March 1 contract. Likewise, the absence of a JavaScript exception is not evidence of preservation. For this field, read back the resulting value and compare it with the independently approved date, then evaluate native and business validity separately.
Validation should therefore ask two questions. Is the field acceptable under its native and business rules? Did the value survive every representation boundary unchanged? The first cannot substitute for the second.
Recover stored dates using trustworthy provenance
Fixing future writes is usually straightforward once the representation boundary is declared. Correcting historical data is different. A stored 2024-02-29 might be the intended leap-day value, or it might be the shifted result of assigning Tokyo local midnight for March 1. A stored 2024-12-31 might be an intentional date, or it might be the Los Angeles local reading of a January 1 UTC bridge. The value alone cannot distinguish those histories.
Treat historical correction as a provenance problem. Require three inputs before changing a record:
A trustworthy original date. This may be an approved source document, an earlier immutable event, a user-confirmed value, or another authoritative system whose semantics are known.
An identified conversion path. Establish whether the affected code read valueAsDate with local getters, assigned a local-midnight Date, preserved the string, or used another adapter.
The applicable execution context. Record the timezone and relevant runtime or component version used when the conversion occurred. Do not substitute the investigator’s current timezone.
With those inputs, reproduce the exact path against literal records and compare the candidate correction with the authoritative original. Patch only the population for which the causal rule and evidence apply. Keep an audit trail containing old value, approved value, reason, source evidence, transformation version, reviewer, and timestamp. Reconcile copies already sent to downstream systems instead of assuming a frontend repair changes historical exports.
Without those inputs, choose HOLD. An unconditional +1 day is especially dangerous because the paired fixture proves opposite-offset asymmetry: Los Angeles exposes the wrong read, Tokyo exposes the wrong write, and many rows do not shift at all. A blanket correction can damage valid records while leaving other causal paths unresolved.
Assign representation ownership to the component or payload owner. That owner defines whether the field is a date-only string or a UTC-bridged Date, documents null behavior, and approves adapter changes. QA owns the regression matrix and verifies that negative controls remain diagnostic. API and data owners approve any wire-format or historical-record correction. Broader frontend JavaScript and testing foundations help teams organize that work, but the correction decision still requires field-specific evidence.
Rerun the 24 cases after changing a date adapter, component wrapper, Playwright dependency, or qualified browser. Preserve a new run record rather than overwriting the earlier one. The source hash changes when source bytes change; changing only the execution environment need not change that hash. If a result differs from the normative contract, preserve the discrepancy and investigate it instead of changing the oracle to restore a green aggregate.
Build stronger frontend reasoning through practice
This defect sits at the intersection of HTML contracts, JavaScript representations, API design, and regression evidence. For structured practice across those foundations, review Refonte Learning’s Frontend Development programme. The programme page lists 10–12 hours per week and describes HTML5/CSS3, JavaScript ES6+, responsive design, React, Redux, APIs/AJAX, Git, accessibility, performance optimization, and a capstone. It advertises a potential internship and says successful completion provides a Training Certificate and a Certificate of Internship. Those statements do not promise individual placement or establish that this exact valueAsDate and Playwright laboratory is included.
For component work, the transferable skill is not memorizing that one timezone showed February 29. It is learning to name the representation before choosing an accessor, constructor, serializer, or test oracle. A browser can follow its documented UTC conversion perfectly while application code still violates the field’s meaning.
Keep the decision visible in code and tests: preserve a date-only value as the normalized date-only string, or cross the required Date bridge with matching UTC fields at both ends.
