Skip to content

Commit 4fb296a

Browse files
committed
PerformanceObserver improvements
1 parent 0675c4d commit 4fb296a

3 files changed

Lines changed: 134 additions & 18 deletions

File tree

‎src/main/java/org/htmlunit/BrowserVersionFeatures.java‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,14 @@ public enum BrowserVersionFeatures {
347347
@BrowserFeature({FF, FF_ESR})
348348
JS_NAVIGATOR_DO_NOT_TRACK_UNSPECIFIED,
349349

350+
/** PerformanceObserver.supportedEntryTypes returns only the entry types supported by Firefox. */
351+
@BrowserFeature({FF, FF_ESR})
352+
JS_PERFORMANCE_OBSERVER_ENTRY_TYPES_FF,
353+
354+
/** PerformanceObserver.supportedEntryTypes returns a frozen array. */
355+
@BrowserFeature({CHROME, EDGE})
356+
JS_PERFORMANCE_OBSERVER_ENTRY_TYPES_FROZEN,
357+
350358
/** Indicates that the {@code Object.getOwnPropertyDescriptor.get} contains name. */
351359
@BrowserFeature({FF, FF_ESR})
352360
JS_PROPERTY_DESCRIPTOR_NAME,

‎src/main/java/org/htmlunit/javascript/host/PerformanceObserver.java‎

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,14 @@
1414
*/
1515
package org.htmlunit.javascript.host;
1616

17+
import static org.htmlunit.BrowserVersionFeatures.JS_PERFORMANCE_OBSERVER_ENTRY_TYPES_FF;
18+
import static org.htmlunit.BrowserVersionFeatures.JS_PERFORMANCE_OBSERVER_ENTRY_TYPES_FROZEN;
19+
20+
import org.htmlunit.BrowserVersion;
1721
import org.htmlunit.corejs.javascript.Function;
1822
import org.htmlunit.corejs.javascript.Scriptable;
1923
import org.htmlunit.corejs.javascript.ScriptableObject;
24+
import org.htmlunit.corejs.javascript.SymbolKey;
2025
import org.htmlunit.corejs.javascript.VarScope;
2126
import org.htmlunit.javascript.HtmlUnitScriptable;
2227
import org.htmlunit.javascript.JavaScriptEngine;
@@ -42,12 +47,21 @@
4247
@JsxClass
4348
public class PerformanceObserver extends HtmlUnitScriptable {
4449

45-
// entry types reported by Chrome/Edge
4650
// Object[] (not String[]) because the array is handed to Rhino, which may store other values
51+
52+
/** The entry types supported by Chrome/Edge. */
4753
private static final Object[] SUPPORTED_ENTRY_TYPES = {
48-
"element", "event", "first-input", "largest-contentful-paint", "layout-shift",
49-
"long-animation-frame", "longtask", "mark", "measure", "navigation", "paint",
50-
"resource", "visibility-state"};
54+
"element", "event", "first-input", "interaction-contentful-paint", "largest-contentful-paint",
55+
"layout-shift", "long-animation-frame", "longtask", "mark", "measure", "navigation", "paint",
56+
"resource", "soft-navigation", "visibility-state"};
57+
58+
/** The entry types supported by Firefox. */
59+
private static final Object[] SUPPORTED_ENTRY_TYPES_FF = {
60+
"event", "first-input", "largest-contentful-paint", "mark", "measure", "navigation", "paint",
61+
"resource"};
62+
63+
/** The key of the cached (frozen) supportedEntryTypes array. */
64+
private static final String SUPPORTED_ENTRY_TYPES_KEY = "PerformanceObserver.supportedEntryTypes";
5165

5266
private enum ObserverType { UNDEFINED, SINGLE, MULTIPLE }
5367

@@ -79,6 +93,16 @@ public void observe(final Object options) {
7993
type = getOptionValue(optionsScriptable, "type");
8094
}
8195

96+
// WebIDL sequence<DOMString>: only iterable objects (e.g. arrays, sets) can be converted
97+
if (entryTypes != null
98+
&& !(entryTypes instanceof Scriptable entryTypesScriptable
99+
&& ScriptableObject.hasProperty(entryTypesScriptable, SymbolKey.ITERATOR))) {
100+
throw JavaScriptEngine.typeError(
101+
"Failed to execute 'observe' on 'PerformanceObserver': "
102+
+ "Failed to read the 'entryTypes' property from 'PerformanceObserverInit': "
103+
+ "The provided value cannot be converted to a sequence.");
104+
}
105+
82106
if (entryTypes == null && type == null) {
83107
throw JavaScriptEngine.typeError(
84108
"Failed to execute 'observe' on 'PerformanceObserver': "
@@ -130,12 +154,56 @@ public Scriptable takeRecords() {
130154
*/
131155
@JsxStaticGetter
132156
public static Scriptable getSupportedEntryTypes(final Scriptable thisObj) {
157+
final Window window = getWindow(thisObj);
158+
final BrowserVersion browserVersion = window.getBrowserVersion();
159+
160+
if (!browserVersion.hasFeature(JS_PERFORMANCE_OBSERVER_ENTRY_TYPES_FROZEN)) {
161+
// a new (modifiable) array for every call
162+
return newEntryTypesArray(thisObj, browserVersion);
163+
}
164+
165+
// FrozenArray<DOMString>; the same object is returned for every call,
166+
// cached at the PerformanceObserver constructor of the window
167+
final ScriptableObject cacheHolder = thisObj instanceof ScriptableObject holder ? holder : null;
168+
if (cacheHolder != null
169+
&& cacheHolder.getAssociatedValue(SUPPORTED_ENTRY_TYPES_KEY) instanceof Scriptable cached) {
170+
return cached;
171+
}
172+
173+
final Scriptable result = newEntryTypesArray(thisObj, browserVersion);
174+
final Object objectCtor = ScriptableObject.getProperty(window, "Object");
175+
if (objectCtor instanceof Scriptable objectCtorScriptable) {
176+
ScriptableObject.callMethod(objectCtorScriptable, "freeze", new Object[] {result});
177+
}
178+
if (cacheHolder != null) {
179+
cacheHolder.associateValue(SUPPORTED_ENTRY_TYPES_KEY, result);
180+
}
181+
return result;
182+
}
183+
184+
/**
185+
* Creates a new array containing the entry types supported by the given browser.
186+
*
187+
* @param thisObj the scriptable (used to determine the scope)
188+
* @param browserVersion the browser version
189+
* @return a new array containing the supported entry types
190+
*/
191+
private static Scriptable newEntryTypesArray(final Scriptable thisObj, final BrowserVersion browserVersion) {
192+
final Object[] entryTypes = browserVersion.hasFeature(JS_PERFORMANCE_OBSERVER_ENTRY_TYPES_FF)
193+
? SUPPORTED_ENTRY_TYPES_FF
194+
: SUPPORTED_ENTRY_TYPES;
195+
133196
final VarScope scope = ScriptableObject.getTopLevelScope(thisObj.getParentScope());
134-
return JavaScriptEngine.newArray(scope, SUPPORTED_ENTRY_TYPES.clone());
197+
return JavaScriptEngine.newArray(scope, entryTypes.clone());
135198
}
136199

137200
/**
201+
* Returns the value of the given member of the options dictionary;
138202
* WebIDL dictionary semantics: a member set to {@code undefined} counts as absent.
203+
*
204+
* @param options the options dictionary
205+
* @param name the name of the member
206+
* @return the value of the member or {@code null} if the member is absent
139207
*/
140208
private static Object getOptionValue(final Scriptable options, final String name) {
141209
final Object value = ScriptableObject.getProperty(options, name);

‎src/test/java/org/htmlunit/javascript/host/PerformanceObserverTest.java‎

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -213,10 +213,6 @@ public void disconnect() throws Exception {
213213
@Alerts(DEFAULT = {"true", "true", "true", "true"},
214214
FF = {"true", "false", "true", "true"},
215215
FF_ESR = {"true", "false", "true", "true"})
216-
@HtmlUnitNYI(CHROME = {"true", "false", "true", "true"},
217-
EDGE = {"true", "false", "true", "true"},
218-
FF = {"true", "false", "true", "true"},
219-
FF_ESR = {"true", "false", "true", "true"})
220216
public void supportedEntryTypes() throws Exception {
221217
runTest("var types = PerformanceObserver.supportedEntryTypes;\n"
222218
+ "log(Array.isArray(types));\n"
@@ -233,19 +229,63 @@ public void supportedEntryTypes() throws Exception {
233229
+ "long-animation-frame,longtask,mark,measure,navigation,paint,resource,soft-navigation,visibility-state",
234230
FF = "event,first-input,largest-contentful-paint,mark,measure,navigation,paint,resource",
235231
FF_ESR = "event,first-input,largest-contentful-paint,mark,measure,navigation,paint,resource")
236-
@HtmlUnitNYI(
237-
CHROME = "element,event,first-input,largest-contentful-paint,layout-shift,"
238-
+ "long-animation-frame,longtask,mark,measure,navigation,paint,resource,visibility-state",
239-
EDGE = "element,event,first-input,largest-contentful-paint,layout-shift,"
240-
+ "long-animation-frame,longtask,mark,measure,navigation,paint,resource,visibility-state",
241-
FF = "element,event,first-input,largest-contentful-paint,layout-shift,"
242-
+ "long-animation-frame,longtask,mark,measure,navigation,paint,resource,visibility-state",
243-
FF_ESR = "element,event,first-input,largest-contentful-paint,layout-shift,"
244-
+ "long-animation-frame,longtask,mark,measure,navigation,paint,resource,visibility-state")
245232
public void supportedEntryTypesList() throws Exception {
246233
runTest("log(PerformanceObserver.supportedEntryTypes.join());");
247234
}
248235

236+
/**
237+
* The entryTypes member has to be a sequence (e.g. an array).
238+
* @throws Exception if the test fails
239+
*/
240+
@Test
241+
@Alerts({"TypeError", "TypeError", "TypeError", "TypeError", "done"})
242+
public void observeEntryTypesNotASequence() throws Exception {
243+
runTest("var observer = new PerformanceObserver(function() {});\n"
244+
+ "try { observer.observe({ entryTypes: 'mark' }); log('done'); } catch(e) { logEx(e); }\n"
245+
+ "try { observer.observe({ entryTypes: 7 }); log('done'); } catch(e) { logEx(e); }\n"
246+
+ "try { observer.observe({ entryTypes: null }); log('done'); } catch(e) { logEx(e); }\n"
247+
+ "try { observer.observe({ entryTypes: {} }); log('done'); } catch(e) { logEx(e); }\n"
248+
+ "try { observer.observe({ entryTypes: new Set(['mark']) }); log('done'); } catch(e) { logEx(e); }");
249+
}
250+
251+
/**
252+
* @throws Exception if the test fails
253+
*/
254+
@Test
255+
@Alerts("done")
256+
public void observeEntryTypesWithBuffered() throws Exception {
257+
runTest("var observer = new PerformanceObserver(function() {});\n"
258+
+ "try { observer.observe({ entryTypes: ['mark'], buffered: true }); log('done'); }"
259+
+ " catch(e) { logEx(e); }");
260+
}
261+
262+
/**
263+
* @throws Exception if the test fails
264+
*/
265+
@Test
266+
@Alerts({"TypeError", "TypeError"})
267+
public void observeOptionsNotAnObject() throws Exception {
268+
runTest("var observer = new PerformanceObserver(function() {});\n"
269+
+ "try { observer.observe('mark'); log('done'); } catch(e) { logEx(e); }\n"
270+
+ "try { observer.observe(null); log('done'); } catch(e) { logEx(e); }");
271+
}
272+
273+
/**
274+
* @throws Exception if the test fails
275+
*/
276+
@Test
277+
@Alerts(DEFAULT = {"true", "TypeError", "-1"},
278+
FF = {"false", "9", "-1"},
279+
FF_ESR = {"false", "9", "-1"})
280+
@HtmlUnitNYI(CHROME = {"true", "15", "-1"},
281+
EDGE = {"true", "15", "-1"})
282+
public void supportedEntryTypesSameObject() throws Exception {
283+
runTest("var types = PerformanceObserver.supportedEntryTypes;\n"
284+
+ "log(types === PerformanceObserver.supportedEntryTypes);\n"
285+
+ "try { types.push('htmlunit'); log(types.length); } catch(e) { logEx(e); }\n"
286+
+ "log(PerformanceObserver.supportedEntryTypes.indexOf('htmlunit'));");
287+
}
288+
249289
private void runTest(final String js) throws Exception {
250290
final String html = DOCTYPE_HTML
251291
+ "<html><head>\n"

0 commit comments

Comments
 (0)