1- import { describe , expect , it , vi } from 'vitest' ;
1+ import { afterEach , describe , expect , it , vi } from 'vitest' ;
22
33async function getFreshPerformanceTimeOrigin ( ) {
44 // Adding the query param with the date, forces a fresh import each time this is called
@@ -7,8 +7,179 @@ async function getFreshPerformanceTimeOrigin() {
77 return timeModule . browserPerformanceTimeOrigin ( ) ;
88}
99
10+ let freshImportCounter = 0 ;
11+
12+ async function getFreshTimestampInSeconds ( ) : Promise < ( ) => number > {
13+ // A counter rather than `Date.now()`: these tests run under fake timers, which freeze the wall clock and would
14+ // otherwise hand out a cached module.
15+ const timeModule = await import ( `../../../src/utils/time?update=${ freshImportCounter ++ } ` ) ;
16+ return timeModule . timestampInSeconds ;
17+ }
18+
1019const RELIABLE_THRESHOLD_MS = 300_000 ;
1120
21+ describe ( 'timestampInSeconds' , ( ) => {
22+ afterEach ( ( ) => {
23+ vi . useRealTimers ( ) ;
24+ vi . unstubAllGlobals ( ) ;
25+ } ) ;
26+
27+ it ( 'derives the timestamp from `performance.timeOrigin` and `performance.now()`' , async ( ) => {
28+ const currentTimeMs = 1767778040866 ;
29+ const timeSincePageloadMs = 1_234.56789 ;
30+
31+ vi . useFakeTimers ( ) ;
32+ vi . setSystemTime ( new Date ( currentTimeMs ) ) ;
33+ vi . stubGlobal ( 'performance' , {
34+ timeOrigin : currentTimeMs - timeSincePageloadMs ,
35+ now : ( ) => timeSincePageloadMs ,
36+ } ) ;
37+
38+ const timestampInSeconds = await getFreshTimestampInSeconds ( ) ;
39+
40+ expect ( timestampInSeconds ( ) ) . toBe ( currentTimeMs / 1000 ) ;
41+ } ) ;
42+
43+ it ( 'falls back to `Date.now()` if the performance API is unavailable' , async ( ) => {
44+ const currentTimeMs = 1767778040866 ;
45+
46+ vi . useFakeTimers ( ) ;
47+ vi . setSystemTime ( new Date ( currentTimeMs ) ) ;
48+ vi . stubGlobal ( 'performance' , undefined ) ;
49+
50+ const timestampInSeconds = await getFreshTimestampInSeconds ( ) ;
51+
52+ expect ( timestampInSeconds ( ) ) . toBe ( currentTimeMs / 1000 ) ;
53+ } ) ;
54+
55+ it ( 'keeps using `performance.timeOrigin` while the clocks agree' , async ( ) => {
56+ const currentTimeMs = 1767778040866 ;
57+ // Below the drift threshold, so the (inaccurate) time origin must be preserved.
58+ const timeOriginSkewMs = RELIABLE_THRESHOLD_MS - 2_000 ;
59+
60+ let timeSincePageloadMs = 1_000 ;
61+
62+ vi . useFakeTimers ( ) ;
63+ vi . setSystemTime ( new Date ( currentTimeMs ) ) ;
64+ vi . stubGlobal ( 'performance' , {
65+ timeOrigin : currentTimeMs - timeSincePageloadMs + timeOriginSkewMs ,
66+ now : ( ) => timeSincePageloadMs ,
67+ } ) ;
68+
69+ const timestampInSeconds = await getFreshTimestampInSeconds ( ) ;
70+
71+ expect ( timestampInSeconds ( ) ) . toBe ( ( currentTimeMs + timeOriginSkewMs ) / 1000 ) ;
72+
73+ timeSincePageloadMs = 5_000 ;
74+ vi . setSystemTime ( new Date ( currentTimeMs + 4_000 ) ) ;
75+
76+ expect ( timestampInSeconds ( ) ) . toBe ( ( currentTimeMs + 4_000 + timeOriginSkewMs ) / 1000 ) ;
77+ } ) ;
78+
79+ it ( 're-derives the time origin once the monotonic clock drifts from the wall clock' , async ( ) => {
80+ const currentTimeMs = 1767778040866 ;
81+ const timeSincePageloadMs = 1_000 ;
82+
83+ // The monotonic clock pauses during sleep, so the wall clock advances much further than it does.
84+ const sleepDurationMs = RELIABLE_THRESHOLD_MS + 60_000 ;
85+
86+ vi . useFakeTimers ( ) ;
87+ vi . setSystemTime ( new Date ( currentTimeMs ) ) ;
88+ vi . stubGlobal ( 'performance' , {
89+ timeOrigin : currentTimeMs - timeSincePageloadMs ,
90+ now : ( ) => timeSincePageloadMs ,
91+ } ) ;
92+
93+ const timestampInSeconds = await getFreshTimestampInSeconds ( ) ;
94+
95+ expect ( timestampInSeconds ( ) ) . toBe ( currentTimeMs / 1000 ) ;
96+
97+ vi . setSystemTime ( new Date ( currentTimeMs + sleepDurationMs ) ) ;
98+
99+ expect ( timestampInSeconds ( ) ) . toBe ( ( currentTimeMs + sleepDurationMs ) / 1000 ) ;
100+ } ) ;
101+
102+ it ( 'keeps deriving elapsed time from the monotonic clock after re-deriving the time origin' , async ( ) => {
103+ const currentTimeMs = 1767778040866 ;
104+ const sleepDurationMs = RELIABLE_THRESHOLD_MS + 60_000 ;
105+
106+ let timeSincePageloadMs = 1_000 ;
107+
108+ vi . useFakeTimers ( ) ;
109+ vi . setSystemTime ( new Date ( currentTimeMs ) ) ;
110+ vi . stubGlobal ( 'performance' , {
111+ timeOrigin : currentTimeMs - timeSincePageloadMs ,
112+ now : ( ) => timeSincePageloadMs ,
113+ } ) ;
114+
115+ const timestampInSeconds = await getFreshTimestampInSeconds ( ) ;
116+
117+ timestampInSeconds ( ) ;
118+ vi . setSystemTime ( new Date ( currentTimeMs + sleepDurationMs ) ) ;
119+ const afterCorrection = timestampInSeconds ( ) ;
120+
121+ // `Date.now()` deliberately stays put while the monotonic clock advances sub-millisecond, proving the elapsed
122+ // time comes from `performance.now()` rather than from the coarser wall clock.
123+ timeSincePageloadMs += 0.25 ;
124+ expect ( timestampInSeconds ( ) ) . toBeCloseTo ( afterCorrection + 0.25 / 1000 , 10 ) ;
125+ } ) ;
126+
127+ it ( 'does not re-derive the time origin repeatedly once the clocks agree again' , async ( ) => {
128+ const currentTimeMs = 1767778040866 ;
129+ const sleepDurationMs = RELIABLE_THRESHOLD_MS + 60_000 ;
130+
131+ let timeSincePageloadMs = 1_000 ;
132+
133+ vi . useFakeTimers ( ) ;
134+ vi . setSystemTime ( new Date ( currentTimeMs ) ) ;
135+ vi . stubGlobal ( 'performance' , {
136+ timeOrigin : currentTimeMs - timeSincePageloadMs ,
137+ now : ( ) => timeSincePageloadMs ,
138+ } ) ;
139+
140+ const timestampInSeconds = await getFreshTimestampInSeconds ( ) ;
141+
142+ timestampInSeconds ( ) ;
143+ vi . setSystemTime ( new Date ( currentTimeMs + sleepDurationMs ) ) ;
144+ timestampInSeconds ( ) ;
145+
146+ // Advance both clocks in lockstep: the re-derived time origin must stay valid, so timestamps track the wall clock
147+ // exactly rather than oscillating between the two sources.
148+ for ( let i = 1 ; i <= 3 ; i ++ ) {
149+ timeSincePageloadMs += 1_000 ;
150+ vi . setSystemTime ( new Date ( currentTimeMs + sleepDurationMs + i * 1_000 ) ) ;
151+ expect ( timestampInSeconds ( ) ) . toBe ( ( currentTimeMs + sleepDurationMs + i * 1_000 ) / 1000 ) ;
152+ }
153+ } ) ;
154+
155+ it ( 'produces monotonically increasing timestamps when the wall clock steps backwards' , async ( ) => {
156+ const currentTimeMs = 1767778040866 ;
157+
158+ let timeSincePageloadMs = 1_000 ;
159+
160+ vi . useFakeTimers ( ) ;
161+ vi . setSystemTime ( new Date ( currentTimeMs ) ) ;
162+ vi . stubGlobal ( 'performance' , {
163+ timeOrigin : currentTimeMs - timeSincePageloadMs ,
164+ now : ( ) => timeSincePageloadMs ,
165+ } ) ;
166+
167+ const timestampInSeconds = await getFreshTimestampInSeconds ( ) ;
168+
169+ const before = timestampInSeconds ( ) ;
170+
171+ // A backwards wall clock step (NTP correction, user changing the clock) beyond the threshold.
172+ vi . setSystemTime ( new Date ( currentTimeMs - RELIABLE_THRESHOLD_MS - 60_000 ) ) ;
173+ timeSincePageloadMs += 1_000 ;
174+ const afterStep = timestampInSeconds ( ) ;
175+
176+ // The correction itself moves the timestamp backwards, but elapsed time afterwards is still monotonic.
177+ timeSincePageloadMs += 1_000 ;
178+ expect ( timestampInSeconds ( ) ) . toBeGreaterThan ( afterStep ) ;
179+ expect ( before ) . toBeGreaterThan ( afterStep ) ;
180+ } ) ;
181+ } ) ;
182+
12183describe ( 'browserPerformanceTimeOrigin' , ( ) => {
13184 it ( 'returns `performance.timeOrigin` if it is available and reliable' , async ( ) => {
14185 const timeOrigin = await getFreshPerformanceTimeOrigin ( ) ;
0 commit comments