import { expect, fixture, html } from '@open-wc/testing'; import './nile-date-time-picker'; import '../nile-calendar'; import '../nile-input'; import type { NileDateTimePicker } from './nile-date-time-picker'; import type { NileCalendar } from '../nile-calendar'; /** * nile-date-time-picker — single consolidated suite. * * Merged from what used to be three files: * nile-date-time-picker.test.ts → sections A–H * nile-date-time-picker.tz-audit.test.ts → section I * nile-date-time-picker.dst-gap.regression.ts → section J * nile-date-time-picker.adversarial.test.ts → section K (was src/_audit/) * * ── ARCHITECTURE (matters for how these tests reach the panel) ─────────────── * The component is a nile-calendar (variant="date-time-picker") plus a * structured-input mask on the trigger input, hosted in a *nile-floating-panel* * (Tippy-backed) — NOT a nile-dropdown. * * The floating panel hands its content to Tippy as the popper `content`, so the * calendar is REPARENTED OUT of the host's shadow root at panel-construction * time (into a detached popper div, moved to document.body on show). Therefore: * * el.shadowRoot.querySelector('nile-calendar') → ALWAYS null * el.calendarElement → the calendar ✅ * * `calendarElement` is the component's public accessor and the only reliable * handle. The trigger input is the panel's anchor and DOES stay in the shadow * root, so it is still queried normally. * * The calendar renders (and is fully interactive) even while the panel is * closed/detached, so tests only open the panel when the assertion is about * panel lifecycle itself (D2). * * ── GRID NOTE ──────────────────────────────────────────────────────────────── * In the date-time-picker variant the day grid is a COLLAPSED 3-week window * (exactly 21 cells) centred on today — not a full month. So the count of * `.day_date:not(.filler)` is date-dependent (≈8–21). Tests index into it and * assert with patterns rather than fixed days. * * The panel's own behavior (day selection, time fields, timezone, set-to-now, * apply/reset, expand/collapse) is exercised in nile-calendar.test.ts. This * suite covers the wrapper's contract: * A. structure / trigger input * B. grouped-config forwarding (fields / value / limits / input / behavior) * C. event payloads + parity with a standalone nile-calendar * D. trigger formatting + panel lifecycle * E. structuredInput mask * F. disabled gating * G. hideTimeFields honored by the trigger * H. timezone carried in the value only * I. timezone correctness audit (wall-clock ⇄ instant math) * J. DST spring-forward gap regression * * All grouped config goes through the object props (fields/value/limits/input/ * behavior/popup) — the only public API the component reads. */ describe('NileDateTimePicker (wrapper)', () => { const dtpFixture = () => fixture(html``); // The calendar lives in a detached Tippy popper — use the public accessor. const innerCalendar = (el: NileDateTimePicker): any => (el as any).calendarElement; const triggerInput = (el: NileDateTimePicker): any => el.shadowRoot!.querySelector('nile-input'); const panelOf = (el: NileDateTimePicker): any => el.shadowRoot!.querySelector('nile-floating-panel'); // settle the wrapper, then its inner calendar const settle = async (el: NileDateTimePicker) => { await el.updateComplete; const cal = innerCalendar(el); if (cal) await cal.updateComplete; return cal; }; // adopting a zone from a preset value sets `timezone`, which schedules a // second update; settle twice so the re-seed lands before we assert. const settle2 = async (el: NileDateTimePicker) => { await settle(el); return settle(el); }; const dayCells = (cal: any): NodeListOf => cal.shadowRoot!.querySelectorAll('.day_date:not(.filler)'); const clickDay = (cal: any, i = 5) => (dayCells(cal)[i] as HTMLElement).click(); const applyBtn = (cal: any) => cal.shadowRoot!.querySelector('.apply-button') as HTMLElement; const resetBtn = (cal: any) => cal.shadowRoot!.querySelector('.clear-button') as HTMLElement; // ── A. structure / trigger input ────────────────────────────────────────── describe('A. structure', () => { it('A1. is defined', () => { expect(customElements.get('nile-date-time-picker')).to.exist; }); it('A2. renders an inner nile-calendar with the date-time-picker variant + footer', async () => { const el = await dtpFixture(); const cal = await settle(el); expect(cal).to.exist; expect(cal.variant).to.equal('date-time-picker'); expect(cal.shadowRoot!.querySelector('.base--date-time-picker')).to.exist; expect(cal.shadowRoot!.querySelector('.tp-footer')).to.exist; }); it('A2b. the calendar is hosted by a nile-floating-panel, outside the shadow root', async () => { const el = await dtpFixture(); const cal = await settle(el); expect(panelOf(el), 'panel host').to.exist; // documents the reparenting contract this suite depends on expect(el.shadowRoot!.querySelector('nile-calendar'), 'moved into the popper').to.be.null; expect(cal.tagName).to.equal('NILE-CALENDAR'); }); // ---- accessible naming of the trigger ---- // The placeholder is a FORMAT MASK ("DD/MM/YY - hh:mm:ss.SSS - timezone"), not a // name, and it is only a last-resort fallback in the accessible-name computation. // So the component offers two explicit naming routes: // `label` → forwarded to nile-input, which names the native input // via its own