From 570e2b59c2ca508454caebc29f673ddcd228debc Mon Sep 17 00:00:00 2001 From: soulgalore Date: Sun, 7 Jan 2024 15:20:43 +0100 Subject: [PATCH] docs: new scripting documentation --- .../sitespeed.io/scripting/Actions.html | 8 + .../sitespeed.io/scripting/AddText.html | 3 + .../scripting/AndroidCommand.html | 3 + .../sitespeed.io/scripting/Cache.html | 3 + .../ChromeDevelopmentToolsProtocol.html | 3 + .../sitespeed.io/scripting/ChromeTrace.html | 3 + .../sitespeed.io/scripting/Click.html | 3 + .../sitespeed.io/scripting/ClickAndHold.html | 3 + .../sitespeed.io/scripting/Commands.html | 3 + .../sitespeed.io/scripting/ContextClick.html | 3 + .../sitespeed.io/scripting/Debug.html | 3 + .../sitespeed.io/scripting/DoubleClick.html | 3 + .../sitespeed.io/scripting/Element.html | 3 + .../sitespeed.io/scripting/GeckoProfiler.html | 3 + .../sitespeed.io/scripting/JavaScript.html | 3 + .../sitespeed.io/scripting/Measure.html | 8 + .../sitespeed.io/scripting/Meta.html | 3 + .../sitespeed.io/scripting/MouseMove.html | 3 + .../sitespeed.io/scripting/Navigation.html | 3 + .../sitespeed.io/scripting/Screenshot.html | 3 + .../sitespeed.io/scripting/Scroll.html | 3 + .../sitespeed.io/scripting/Select.html | 3 + .../sitespeed.io/scripting/Set.html | 3 + .../sitespeed.io/scripting/SingleClick.html | 3 + .../sitespeed.io/scripting/StopWatch.html | 3 + .../sitespeed.io/scripting/Switch.html | 3 + .../sitespeed.io/scripting/Wait.html | 3 + .../sitespeed.io/scripting/data/search.json | 1 + .../scripting/fonts/Inconsolata-Regular.ttf | Bin 0 -> 97864 bytes .../scripting/fonts/OpenSans-Regular.ttf | Bin 0 -> 129796 bytes .../scripting/fonts/WorkSans-Bold.ttf | Bin 0 -> 192548 bytes .../sitespeed.io/scripting/index.html | 3 + .../sitespeed.io/scripting/index.md | 1655 ------ .../sitespeed.io/scripting/scripts/core.js | 702 +++ .../scripting/scripts/core.min.js | 23 + .../sitespeed.io/scripting/scripts/resize.js | 90 + .../sitespeed.io/scripting/scripts/search.js | 265 + .../scripting/scripts/search.min.js | 6 + .../third-party/Apache-License-2.0.txt | 202 + .../scripting/scripts/third-party/fuse.js | 9 + .../third-party/hljs-line-num-original.js | 369 ++ .../scripts/third-party/hljs-line-num.js | 1 + .../scripts/third-party/hljs-original.js | 5171 +++++++++++++++++ .../scripting/scripts/third-party/hljs.js | 1 + .../scripting/scripts/third-party/popper.js | 5 + .../scripting/scripts/third-party/tippy.js | 1 + .../scripting/scripts/third-party/tocbot.js | 672 +++ .../scripts/third-party/tocbot.min.js | 1 + .../styles/clean-jsdoc-theme-base.css | 975 ++++ .../styles/clean-jsdoc-theme-dark.css | 407 ++ .../styles/clean-jsdoc-theme-light.css | 388 ++ .../styles/clean-jsdoc-theme.min.css | 1 + .../scripting/tutorial-01-Introduction.html | 19 + .../tutorial-02-Running-Scripts.html | 29 + .../tutorial-03-Measurement-Commands.html | 76 + .../tutorial-04-Interact-with-the-page.html | 68 + .../tutorial-05-Interact-Browser.html | 31 + .../scripting/tutorial-06-Error-handling.html | 48 + .../tutorial-07-Debugging-Scripts.html | 33 + .../tutorial-08-Setting-Up-IntelliSense.html | 11 + .../scripting/tutorial-09-Examples.html | 307 + .../scripting/tutorial-10-Selenium.html | 40 + .../tutorial-11-Chrome-Devtools-Protocol.html | 71 + .../scripting/tutorial-12-Android.html | 29 + .../tutorial-13-Tips-and-tricks.html | 110 + 65 files changed, 10256 insertions(+), 1655 deletions(-) create mode 100644 docs/documentation/sitespeed.io/scripting/Actions.html create mode 100644 docs/documentation/sitespeed.io/scripting/AddText.html create mode 100644 docs/documentation/sitespeed.io/scripting/AndroidCommand.html create mode 100644 docs/documentation/sitespeed.io/scripting/Cache.html create mode 100644 docs/documentation/sitespeed.io/scripting/ChromeDevelopmentToolsProtocol.html create mode 100644 docs/documentation/sitespeed.io/scripting/ChromeTrace.html create mode 100644 docs/documentation/sitespeed.io/scripting/Click.html create mode 100644 docs/documentation/sitespeed.io/scripting/ClickAndHold.html create mode 100644 docs/documentation/sitespeed.io/scripting/Commands.html create mode 100644 docs/documentation/sitespeed.io/scripting/ContextClick.html create mode 100644 docs/documentation/sitespeed.io/scripting/Debug.html create mode 100644 docs/documentation/sitespeed.io/scripting/DoubleClick.html create mode 100644 docs/documentation/sitespeed.io/scripting/Element.html create mode 100644 docs/documentation/sitespeed.io/scripting/GeckoProfiler.html create mode 100644 docs/documentation/sitespeed.io/scripting/JavaScript.html create mode 100644 docs/documentation/sitespeed.io/scripting/Measure.html create mode 100644 docs/documentation/sitespeed.io/scripting/Meta.html create mode 100644 docs/documentation/sitespeed.io/scripting/MouseMove.html create mode 100644 docs/documentation/sitespeed.io/scripting/Navigation.html create mode 100644 docs/documentation/sitespeed.io/scripting/Screenshot.html create mode 100644 docs/documentation/sitespeed.io/scripting/Scroll.html create mode 100644 docs/documentation/sitespeed.io/scripting/Select.html create mode 100644 docs/documentation/sitespeed.io/scripting/Set.html create mode 100644 docs/documentation/sitespeed.io/scripting/SingleClick.html create mode 100644 docs/documentation/sitespeed.io/scripting/StopWatch.html create mode 100644 docs/documentation/sitespeed.io/scripting/Switch.html create mode 100644 docs/documentation/sitespeed.io/scripting/Wait.html create mode 100644 docs/documentation/sitespeed.io/scripting/data/search.json create mode 100644 docs/documentation/sitespeed.io/scripting/fonts/Inconsolata-Regular.ttf create mode 100644 docs/documentation/sitespeed.io/scripting/fonts/OpenSans-Regular.ttf create mode 100644 docs/documentation/sitespeed.io/scripting/fonts/WorkSans-Bold.ttf create mode 100644 docs/documentation/sitespeed.io/scripting/index.html delete mode 100644 docs/documentation/sitespeed.io/scripting/index.md create mode 100644 docs/documentation/sitespeed.io/scripting/scripts/core.js create mode 100644 docs/documentation/sitespeed.io/scripting/scripts/core.min.js create mode 100644 docs/documentation/sitespeed.io/scripting/scripts/resize.js create mode 100644 docs/documentation/sitespeed.io/scripting/scripts/search.js create mode 100644 docs/documentation/sitespeed.io/scripting/scripts/search.min.js create mode 100644 docs/documentation/sitespeed.io/scripting/scripts/third-party/Apache-License-2.0.txt create mode 100644 docs/documentation/sitespeed.io/scripting/scripts/third-party/fuse.js create mode 100644 docs/documentation/sitespeed.io/scripting/scripts/third-party/hljs-line-num-original.js create mode 100644 docs/documentation/sitespeed.io/scripting/scripts/third-party/hljs-line-num.js create mode 100644 docs/documentation/sitespeed.io/scripting/scripts/third-party/hljs-original.js create mode 100644 docs/documentation/sitespeed.io/scripting/scripts/third-party/hljs.js create mode 100644 docs/documentation/sitespeed.io/scripting/scripts/third-party/popper.js create mode 100644 docs/documentation/sitespeed.io/scripting/scripts/third-party/tippy.js create mode 100644 docs/documentation/sitespeed.io/scripting/scripts/third-party/tocbot.js create mode 100644 docs/documentation/sitespeed.io/scripting/scripts/third-party/tocbot.min.js create mode 100644 docs/documentation/sitespeed.io/scripting/styles/clean-jsdoc-theme-base.css create mode 100644 docs/documentation/sitespeed.io/scripting/styles/clean-jsdoc-theme-dark.css create mode 100644 docs/documentation/sitespeed.io/scripting/styles/clean-jsdoc-theme-light.css create mode 100644 docs/documentation/sitespeed.io/scripting/styles/clean-jsdoc-theme.min.css create mode 100644 docs/documentation/sitespeed.io/scripting/tutorial-01-Introduction.html create mode 100644 docs/documentation/sitespeed.io/scripting/tutorial-02-Running-Scripts.html create mode 100644 docs/documentation/sitespeed.io/scripting/tutorial-03-Measurement-Commands.html create mode 100644 docs/documentation/sitespeed.io/scripting/tutorial-04-Interact-with-the-page.html create mode 100644 docs/documentation/sitespeed.io/scripting/tutorial-05-Interact-Browser.html create mode 100644 docs/documentation/sitespeed.io/scripting/tutorial-06-Error-handling.html create mode 100644 docs/documentation/sitespeed.io/scripting/tutorial-07-Debugging-Scripts.html create mode 100644 docs/documentation/sitespeed.io/scripting/tutorial-08-Setting-Up-IntelliSense.html create mode 100644 docs/documentation/sitespeed.io/scripting/tutorial-09-Examples.html create mode 100644 docs/documentation/sitespeed.io/scripting/tutorial-10-Selenium.html create mode 100644 docs/documentation/sitespeed.io/scripting/tutorial-11-Chrome-Devtools-Protocol.html create mode 100644 docs/documentation/sitespeed.io/scripting/tutorial-12-Android.html create mode 100644 docs/documentation/sitespeed.io/scripting/tutorial-13-Tips-and-tricks.html diff --git a/docs/documentation/sitespeed.io/scripting/Actions.html b/docs/documentation/sitespeed.io/scripting/Actions.html new file mode 100644 index 0000000000..63fba24722 --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/Actions.html @@ -0,0 +1,8 @@ +Class: Actions
On this page

Actions

This class provides an abstraction layer for Selenium's action sequence functionality. It allows for easy interaction with web elements using different locating strategies and simulating complex user gestures like mouse movements, key presses, etc.

Classes

Actions

Methods

getActions() → {SeleniumActions}

Retrieves the current action sequence builder. The actions builder can be used to chain multiple browser actions.
Returns:
The current Selenium Actions builder object for chaining browser actions.
Type: 
SeleniumActions
Example
// Example of using the actions builder to perform a drag-and-drop operation:
+const elementToDrag = await commands.action.getElementByCss('.draggable');
+const dropTarget = await commands.action.getElementByCss('.drop-target');
+await commands.action.getAction()
+  .dragAndDrop(elementToDrag, dropTarget)
+  .perform();
\ No newline at end of file diff --git a/docs/documentation/sitespeed.io/scripting/AddText.html b/docs/documentation/sitespeed.io/scripting/AddText.html new file mode 100644 index 0000000000..95ca1d3b6f --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/AddText.html @@ -0,0 +1,3 @@ +Class: AddText
On this page

AddText

Provides functionality to add text to elements on a web page using various selectors.

Classes

AddText

Methods

(async) byClassName(text, className) → {Promise.<void>}

Adds text to an element identified by its class name.
Parameters:
NameTypeDescription
textstringThe text string to add.
classNamestringThe class name of the element.
Throws:
Throws an error if the element is not found.
Type
Error
Returns:
A promise that resolves when the text has been added.
Type: 
Promise.<void>
Example
commands.addText.byClassName('mytext', 'className');

(async) byId(text, id) → {Promise.<void>}

Adds text to an element identified by its ID.
Parameters:
NameTypeDescription
textstringThe text string to add.
idstringThe ID of the element.
Throws:
Throws an error if the element is not found.
Type
Error
Returns:
A promise that resolves when the text has been added.
Type: 
Promise.<void>
Example
commands.addText.byId('mytext', 'id');

(async) byName(text, name) → {Promise.<void>}

Adds text to an element identified by its name attribute.
Parameters:
NameTypeDescription
textstringThe text string to add.
namestringThe name attribute of the element.
Throws:
Throws an error if the element is not found.
Type
Error
Returns:
A promise that resolves when the text has been added.
Type: 
Promise.<void>
Example
commands.addText.byName('mytext', 'name');

(async) bySelector(text, selector) → {Promise.<void>}

Adds text to an element identified by its CSS selector.
Parameters:
NameTypeDescription
textstringThe text string to add.
selectorstringThe CSS selector of the element.
Throws:
Throws an error if the element is not found.
Type
Error
Returns:
A promise that resolves when the text has been added.
Type: 
Promise.<void>
Example
commands.addText.bySelector('mytext', 'selector');

(async) byXpath(text, xpath) → {Promise.<void>}

Adds text to an element identified by its XPath.
Parameters:
NameTypeDescription
textstringThe text string to add.
xpathstringThe XPath of the element.
Throws:
Throws an error if the element is not found.
Type
Error
Returns:
A promise that resolves when the text has been added.
Type: 
Promise.<void>
Example
commands.addText.byXpath('mytext', 'xpath');
\ No newline at end of file diff --git a/docs/documentation/sitespeed.io/scripting/AndroidCommand.html b/docs/documentation/sitespeed.io/scripting/AndroidCommand.html new file mode 100644 index 0000000000..88a872fad8 --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/AndroidCommand.html @@ -0,0 +1,3 @@ +Class: AndroidCommand
On this page

AndroidCommand

Provides functionality to interact with an Android device through shell commands.

Classes

AndroidCommand

Methods

(async) shell(command) → {Promise.<string>}

Runs a shell command on the connected Android device. This method requires the Android device to be properly configured.
Parameters:
NameTypeDescription
commandstringThe shell command to run on the Android device.
Throws:
Throws an error if Android is not configured or if the command fails.
Type
Error
Returns:
A promise that resolves with the result of the command or rejects if there's an error.
Type: 
Promise.<string>
Example
await commands.android.shell('');

(async) shellAsRoot(command) → {Promise.<string>}

Runs a shell command on the connected Android device as the root user. This method requires the Android device to be properly configured and that you rooted the device.
Parameters:
NameTypeDescription
commandstringThe shell command to run on the Android device as root.
Throws:
Throws an error if Android is not configured or if the command fails.
Type
Error
Returns:
A promise that resolves with the result of the command or rejects if there's an error.
Type: 
Promise.<string>
Example
await commands.android.shellAsRoot('');
\ No newline at end of file diff --git a/docs/documentation/sitespeed.io/scripting/Cache.html b/docs/documentation/sitespeed.io/scripting/Cache.html new file mode 100644 index 0000000000..b4cf214aa4 --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/Cache.html @@ -0,0 +1,3 @@ +Class: Cache
On this page

Cache

Manage the browser cache. This class provides methods to clear the cache and cookies in different browsers.

Classes

Cache

Methods

(async) clear() → {Promise.<void>}

Clears the browser cache. This includes both cache and cookies. For Firefox, it uses the extensionServer setup with specific options. For Chrome and Edge, it uses the Chrome DevTools Protocol (CDP) commands. If the browser is not supported, logs an error message.
Throws:
Will throw an error if the browser is not supported.
Returns:
A promise that resolves when the cache and cookies are cleared.
Type: 
Promise.<void>
Example
await commands.cache.clear();

(async) clearKeepCookies() → {Promise.<void>}

Clears the browser cache while keeping the cookies. For Firefox, it uses the extensionServer setup with specific options. For Chrome and Edge, it uses the Chrome DevTools Protocol (CDP) command to clear the cache. If the browser is not supported, logs an error message.
Throws:
Will throw an error if the browser is not supported.
Returns:
A promise that resolves when the cache is cleared but cookies are kept.
Type: 
Promise.<void>
Example
await commands.cache.clearKeepCookies();
\ No newline at end of file diff --git a/docs/documentation/sitespeed.io/scripting/ChromeDevelopmentToolsProtocol.html b/docs/documentation/sitespeed.io/scripting/ChromeDevelopmentToolsProtocol.html new file mode 100644 index 0000000000..683b8d72e0 --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/ChromeDevelopmentToolsProtocol.html @@ -0,0 +1,3 @@ +Class: ChromeDevelopmentToolsProtocol
On this page

ChromeDevelopmentToolsProtocol

Manages interactions with the Chrome DevTools Protocol for Chrome and Edge browsers. Allows sending commands and setting up event listeners via the protocol.

Classes

ChromeDevelopmentToolsProtocol

Methods

getRawClient() → {Object}

Retrieves the raw client for the DevTools Protocol.
Throws:
Throws an error if the browser is not supported.
Type
Error
Returns:
The raw DevTools Protocol client.
Type: 
Object
Example
const cdpClient = commands.cdp.getRawClient();

(async) on(event, f)

Sets up an event listener for a specific DevTools Protocol event.
Parameters:
NameTypeDescription
eventstringThe name of the event to listen for.
ffunctionThe callback function to execute when the event is triggered.
Throws:
Throws an error if the browser is not supported or if setting the listener fails.
Type
Error

(async) send(command, arguments_) → {Promise.<void>}

Sends a command to the DevTools Protocol.
Parameters:
NameTypeDescription
commandstringThe DevTools Protocol command to send.
arguments_ObjectThe arguments for the command.
Throws:
Throws an error if the browser is not supported or if the command fails.
Type
Error
Returns:
A promise that resolves when the command has been sent.
Type: 
Promise.<void>
Example
await commands.cdp.send('');

(async) sendAndGet(command, arguments_) → {Promise.<Object>}

Sends a command to the DevTools Protocol and returns the result.
Parameters:
NameTypeDescription
commandstringThe DevTools Protocol command to send.
arguments_ObjectThe arguments for the command.
Throws:
Throws an error if the browser is not supported or if the command fails.
Type
Error
Returns:
The result of the command execution.
Type: 
Promise.<Object>
Example
const domCounters = await commands.cdp.sendAndGet('Memory.getDOMCounters');
\ No newline at end of file diff --git a/docs/documentation/sitespeed.io/scripting/ChromeTrace.html b/docs/documentation/sitespeed.io/scripting/ChromeTrace.html new file mode 100644 index 0000000000..544e84b5e3 --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/ChromeTrace.html @@ -0,0 +1,3 @@ +Class: ChromeTrace
On this page

ChromeTrace

Manages Chrome trace functionality, enabling custom profiling and trace collection in Chrome.

Classes

ChromeTrace

Methods

(async) start() → {Promise.<void>}

Starts the Chrome trace collection.
Throws:
Throws an error if not running Chrome or if configuration is not set for custom tracing.
Type
Error
Returns:
A promise that resolves when tracing is started.
Type: 
Promise.<void>
Example
await commands.trace.start();

(async) stop() → {Promise.<void>}

Stops the Chrome trace collection, processes the collected data, and attaches it to the result object.
Throws:
Throws an error if not running Chrome or if custom tracing was not started.
Type
Error
Returns:
A promise that resolves when tracing is stopped and data is processed.
Type: 
Promise.<void>
Example
await commands.trace.stop();
\ No newline at end of file diff --git a/docs/documentation/sitespeed.io/scripting/Click.html b/docs/documentation/sitespeed.io/scripting/Click.html new file mode 100644 index 0000000000..1842e26ab7 --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/Click.html @@ -0,0 +1,3 @@ +Class: Click
On this page

Click

Provides functionality to perform click actions on elements in a web page using various selectors.

Classes

Click

Methods

(async) byClassName(className) → {Promise.<void>}

Clicks on an element identified by its class name.
Parameters:
NameTypeDescription
classNamestringThe class name of the element to click.
Throws:
Throws an error if the element is not found.
Type
Error
Returns:
A promise that resolves when the click action is performed.
Type: 
Promise.<void>

(async) byClassNameAndWait(className) → {Promise.<void>}

Clicks on an element identified by its class name and waits for the page complete check to finish.
Parameters:
NameTypeDescription
classNamestringThe class name of the element to click.
Throws:
Throws an error if the element is not found.
Type
Error
Returns:
A promise that resolves when the click action and page complete check are finished.
Type: 
Promise.<void>

(async) byId(id) → {Promise.<void>}

Clicks on an element located by its ID.
Parameters:
NameTypeDescription
idstringThe ID of the element to click.
Throws:
Throws an error if the element is not found.
Type
Error
Returns:
A promise that resolves when the click action is performed.
Type: 
Promise.<void>

(async) byIdAndWait(id) → {Promise.<void>}

Click on link located by the ID attribute. Uses document.getElementById() to find the element. And wait for page complete check to finish.
Parameters:
NameTypeDescription
idstring
Throws:
Will throw an error if the element is not found
Returns:
Promise object represents when the element has been clicked and the pageCompleteCheck has finished.
Type: 
Promise.<void>

(async) byJs(js) → {Promise.<void>}

Clicks on an element located by evaluating a JavaScript expression.
Parameters:
NameTypeDescription
jsstringThe JavaScript expression that evaluates to an element or list of elements.
Throws:
Throws an error if the element is not found.
Type
Error
Returns:
A promise that resolves when the click action is performed.
Type: 
Promise.<void>

(async) byJsAndWait(js) → {Promise.<void>}

Clicks on an element located by evaluating a JavaScript expression and waits for the page complete check to finish.
Parameters:
NameTypeDescription
jsstringThe JavaScript expression that evaluates to an element or list of elements.
Throws:
Throws an error if the element is not found.
Type
Error
Returns:
A promise that resolves when the click action and page complete check are finished.
Type: 
Promise.<void>

(async) byLinkText(text) → {Promise.<void>}

Clicks on a link whose visible text matches the given string.
Parameters:
NameTypeDescription
textstringThe visible text of the link to click.
Throws:
Throws an error if the link is not found.
Type
Error
Returns:
A promise that resolves when the click action is performed.
Type: 
Promise.<void>

(async) byLinkTextAndWait(text) → {Promise.<void>}

Clicks on a link whose visible text matches the given string and waits for the page complete check to finish.
Parameters:
NameTypeDescription
textstringThe visible text of the link to click.
Throws:
Throws an error if the link is not found.
Type
Error
Returns:
A promise that resolves when the click action and page complete check are finished.
Type: 
Promise.<void>

(async) byName(name) → {Promise.<void>}

Clicks on an element located by its name attribute.
Parameters:
NameTypeDescription
namestringThe name attribute of the element to click.
Throws:
Throws an error if the element is not found.
Type
Error
Returns:
A promise that resolves when the click action is performed.
Type: 
Promise.<void>

(async) byPartialLinkText(text) → {Promise.<void>}

Clicks on a link whose visible text contains the given substring.
Parameters:
NameTypeDescription
textstringThe substring of the visible text of the link to click.
Throws:
Throws an error if the link is not found.
Type
Error
Returns:
A promise that resolves when the click action is performed.
Type: 
Promise.<void>

(async) byPartialLinkTextAndWait(text) → {Promise.<void>}

Clicks on a link whose visible text contains the given substring and waits for the page complete check to finish.
Parameters:
NameTypeDescription
textstringThe substring of the visible text of the link to click.
Throws:
Throws an error if the link is not found.
Type
Error
Returns:
A promise that resolves when the click action and page complete check are finished.
Type: 
Promise.<void>

(async) bySelector(selector) → {Promise.<void>}

Clicks on an element located by its CSS selector.
Parameters:
NameTypeDescription
selectorstringThe CSS selector of the element to click.
Throws:
Throws an error if the element is not found.
Type
Error
Returns:
A promise that resolves when the click action is performed.
Type: 
Promise.<void>

(async) bySelectorAndWait(selector) → {Promise.<void>}

Clicks on an element located by its CSS selector and waits for the page complete check to finish.
Parameters:
NameTypeDescription
selectorstringThe CSS selector of the element to click.
Throws:
Throws an error if the element is not found.
Type
Error
Returns:
A promise that resolves when the click action and page complete check are finished.
Type: 
Promise.<void>

(async) byXpath(xpath) → {Promise.<void>}

Clicks on an element that matches a given XPath selector.
Parameters:
NameTypeDescription
xpathstringThe XPath selector of the element to click.
Throws:
Throws an error if the element is not found.
Type
Error
Returns:
A promise that resolves when the click action is performed.
Type: 
Promise.<void>

(async) byXpathAndWait(xpath) → {Promise.<void>}

Clicks on an element that matches a given XPath selector and waits for the page complete check to finish.
Parameters:
NameTypeDescription
xpathstringThe XPath selector of the element to click.
Throws:
Throws an error if the element is not found.
Type
Error
Returns:
A promise that resolves when the click action and page complete check are finished.
Type: 
Promise.<void>
\ No newline at end of file diff --git a/docs/documentation/sitespeed.io/scripting/ClickAndHold.html b/docs/documentation/sitespeed.io/scripting/ClickAndHold.html new file mode 100644 index 0000000000..00455ea2a6 --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/ClickAndHold.html @@ -0,0 +1,3 @@ +Class: ClickAndHold
On this page

ClickAndHold

Provides functionality to click and hold elements on a web page using different strategies.

Classes

ClickAndHold

Methods

(async) atCursor() → {Promise.<void>}

Clicks and holds at the current cursor position.
Throws:
Throws an error if the action cannot be performed.
Type
Error
Returns:
A promise that resolves when the action is performed.
Type: 
Promise.<void>

(async) atPosition(xPos, yPos) → {Promise.<void>}

Clicks and holds at the specified screen coordinates.
Parameters:
NameTypeDescription
xPosnumberThe x-coordinate on the screen.
yPosnumberThe y-coordinate on the screen.
Throws:
Throws an error if the action cannot be performed.
Type
Error
Returns:
A promise that resolves when the action is performed.
Type: 
Promise.<void>

(async) bySelector(selector) → {Promise.<void>}

Clicks and holds on an element that matches a given CSS selector.
Parameters:
NameTypeDescription
selectorstringThe CSS selector of the element to interact with.
Throws:
Throws an error if the element is not found.
Type
Error
Returns:
A promise that resolves when the action is performed.
Type: 
Promise.<void>

(async) byXpath(xpath) → {Promise.<void>}

Clicks and holds on an element that matches a given XPath selector.
Parameters:
NameTypeDescription
xpathstringThe XPath selector of the element to interact with.
Throws:
Throws an error if the element is not found.
Type
Error
Returns:
A promise that resolves when the action is performed.
Type: 
Promise.<void>

(async) releaseAtPosition(xPos, yPos) → {Promise.<void>}

Releases the mouse button at the specified screen coordinates.
Parameters:
NameTypeDescription
xPosnumberThe x-coordinate on the screen.
yPosnumberThe y-coordinate on the screen.
Throws:
Throws an error if the action cannot be performed.
Type
Error
Returns:
A promise that resolves when the action is performed.
Type: 
Promise.<void>

(async) releaseAtSelector(selector) → {Promise.<void>}

Releases the mouse button on an element matching the specified CSS selector.
Parameters:
NameTypeDescription
selectorstringThe CSS selector of the element to release the mouse on.
Throws:
Throws an error if the action cannot be performed.
Type
Error
Returns:
A promise that resolves when the action is performed.
Type: 
Promise.<void>

(async) releaseAtXpath(xpath) → {Promise.<void>}

Releases the mouse button on an element matching the specified XPath.
Parameters:
NameTypeDescription
xpathstringThe XPath selector of the element to release the mouse on.
Throws:
Throws an error if the action cannot be performed.
Type
Error
Returns:
A promise that resolves when the action is performed.
Type: 
Promise.<void>
\ No newline at end of file diff --git a/docs/documentation/sitespeed.io/scripting/Commands.html b/docs/documentation/sitespeed.io/scripting/Commands.html new file mode 100644 index 0000000000..d102e8b2a9 --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/Commands.html @@ -0,0 +1,3 @@ +Class: Commands
On this page

Commands

Represents the set of commands available in a Browsertime script.

Classes

Commands

Members

action :Actions

Selenium's action sequence functionality.
Type:

addText :AddText

Provides functionality to add text to elements on a web page using various selectors.
Type:

android :AndroidCommand

Provides commands for interacting with an Android device.

cache :Cache

Manages the browser's cache.
Type:

cdp :ChromeDevelopmentToolsProtocol

Use the Chrome DevTools Protocol, available in Chrome and Edge.

click :Click

Provides functionality to perform click actions on elements in a web page using various selectors.
Type:

debug :Debug

Provides debugging capabilities within a browser automation script. It allows setting breakpoints to pause script execution and inspect the current state.
Type:

element :Element

Get Selenium's WebElements.
Type:

error :function

Add a text that will be an error attached to the current page.
Type:
  • function
Example
await commands.error('My error message');

js :JavaScript

Executes JavaScript in the browser context.

markAsFailure :function

Mark this run as an failure. Add a message that explains the failure.
Type:
  • function
Example
await commands.markAsFailure('My failure message');

measure :Measure

Provides functionality for measuring a navigation.
Type:

meta :Meta

Adds metadata to the user journey.
Type:

mouse :Object

Interact with the page using the mouse.
Type:
  • Object
Navigates to a specified URL and handles additional setup for a page visit.
Type:
  • function
Example
await commands.navigate('https://www.example.org');
Provides functionality to control browser navigation such as back, forward, and refresh actions.

screenshot :Screenshot

Takes and manages screenshots.

scroll :Scroll

Provides functionality to control page scrolling in the browser.
Type:

select :Select

Interact with a select element.
Type:

set :Set

Sets values on HTML elements in the page.
Type:

stopWatch :StopWatch

Stopwatch utility for measuring time intervals.

switch :Switch

Switches context to different frames, windows, or tabs in the browser.
Type:

trace :ChromeTrace

Manages Chrome trace functionality, enabling custom profiling and trace collection in Chrome.

wait :Wait

Provides functionality to wait for different conditions in the browser.
Type:
\ No newline at end of file diff --git a/docs/documentation/sitespeed.io/scripting/ContextClick.html b/docs/documentation/sitespeed.io/scripting/ContextClick.html new file mode 100644 index 0000000000..bf0e66dc6c --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/ContextClick.html @@ -0,0 +1,3 @@ +Class: ContextClick
On this page

ContextClick

Provides functionality to perform a context click (right-click) on elements in a web page.

Classes

ContextClick

Methods

(async) atCursor() → {Promise.<void>}

Performs a context click (right-click) at the current cursor position.
Throws:
Throws an error if the context click action cannot be performed.
Type
Error
Returns:
A promise that resolves when the context click action is performed.
Type: 
Promise.<void>

(async) bySelector(selector) → {Promise.<void>}

Performs a context click (right-click) on an element that matches a given CSS selector.
Parameters:
NameTypeDescription
selectorstringThe CSS selector of the element to context click.
Throws:
Throws an error if the element is not found.
Type
Error
Returns:
A promise that resolves when the context click action is performed.
Type: 
Promise.<void>

(async) byXpath(xpath) → {Promise.<void>}

Performs a context click (right-click) on an element that matches a given XPath selector.
Parameters:
NameTypeDescription
xpathstringThe XPath selector of the element to context click.
Throws:
Throws an error if the element is not found.
Type
Error
Returns:
A promise that resolves when the context click action is performed.
Type: 
Promise.<void>
\ No newline at end of file diff --git a/docs/documentation/sitespeed.io/scripting/Debug.html b/docs/documentation/sitespeed.io/scripting/Debug.html new file mode 100644 index 0000000000..d3a536b00e --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/Debug.html @@ -0,0 +1,3 @@ +Class: Debug
On this page

Debug

Provides debugging capabilities within a browser automation script. It allows setting breakpoints to pause script execution and inspect the current state.

Classes

Debug

Methods

(async) breakpoint(nameopt) → {Promise.<void>}

Adds a breakpoint to the script. The browser will pause at the breakpoint, waiting for user input to continue. This is useful for debugging and inspecting the browser state at a specific point in the script.
Parameters:
NameTypeAttributesDescription
namestring<optional>
An optional name for the breakpoint for logging purposes.
Returns:
A promise that resolves when the user chooses to continue from the breakpoint.
Type: 
Promise.<void>
\ No newline at end of file diff --git a/docs/documentation/sitespeed.io/scripting/DoubleClick.html b/docs/documentation/sitespeed.io/scripting/DoubleClick.html new file mode 100644 index 0000000000..a2cdf6ece1 --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/DoubleClick.html @@ -0,0 +1,3 @@ +Class: DoubleClick
On this page

DoubleClick

Provides functionality to perform a double-click action on elements in a web page.

Classes

DoubleClick

Methods

(async) atCursor(optionsopt) → {Promise.<void>}

Performs a mouse double-click at the current cursor position.
Parameters:
NameTypeAttributesDescription
optionsObject<optional>
Additional options for the double-click action.
Throws:
Throws an error if the double-click action cannot be performed.
Type
Error
Returns:
A promise that resolves when the double-click occurs.
Type: 
Promise.<void>

(async) bySelector(selector, optionsopt) → {Promise.<void>}

Performs a mouse double-click on an element matching a given CSS selector.
Parameters:
NameTypeAttributesDescription
selectorstringThe CSS selector of the element to double-click.
optionsObject<optional>
Additional options for the double-click action.
Throws:
Throws an error if the element is not found.
Type
Error
Returns:
A promise that resolves when the double-click action is performed.
Type: 
Promise.<void>

(async) byXpath(xpath, optionsopt) → {Promise.<void>}

Performs a mouse double-click on an element matching a given XPath selector.
Parameters:
NameTypeAttributesDescription
xpathstringThe XPath selector of the element to double-click.
optionsObject<optional>
Additional options for the double-click action.
Throws:
Throws an error if the element is not found.
Type
Error
Returns:
A promise that resolves when the double-click action is performed.
Type: 
Promise.<void>
\ No newline at end of file diff --git a/docs/documentation/sitespeed.io/scripting/Element.html b/docs/documentation/sitespeed.io/scripting/Element.html new file mode 100644 index 0000000000..8a05eb8c7a --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/Element.html @@ -0,0 +1,3 @@ +Class: Element
On this page

Element

This class provides a way to get hokld of Seleniums WebElements.

Classes

Element

Methods

(async) getByClassName(className) → {Promise.<WebElement>}

Finds an element by its class name.
Parameters:
NameTypeDescription
classNamestringThe class name of the element.
Returns:
A promise that resolves to the WebElement found.
Type: 
Promise.<WebElement>

(async) getByCss(name) → {Promise.<WebElement>}

Finds an element by its CSS selector.
Parameters:
NameTypeDescription
namestringThe CSS selector of the element.
Returns:
A promise that resolves to the WebElement found.
Type: 
Promise.<WebElement>

(async) getById(id) → {Promise.<WebElement>}

Finds an element by its ID.
Parameters:
NameTypeDescription
idstringThe ID of the element.
Returns:
A promise that resolves to the WebElement found.
Type: 
Promise.<WebElement>

(async) getByName(name) → {Promise.<WebElement>}

Finds an element by its name attribute.
Parameters:
NameTypeDescription
namestringThe name attribute of the element.
Returns:
A promise that resolves to the WebElement found.
Type: 
Promise.<WebElement>

(async) getByXpath(xpath) → {Promise.<WebElement>}

Finds an element by its XPath.
Parameters:
NameTypeDescription
xpathstringThe XPath query of the element.
Returns:
A promise that resolves to the WebElement found.
Type: 
Promise.<WebElement>
\ No newline at end of file diff --git a/docs/documentation/sitespeed.io/scripting/GeckoProfiler.html b/docs/documentation/sitespeed.io/scripting/GeckoProfiler.html new file mode 100644 index 0000000000..631f2ef27a --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/GeckoProfiler.html @@ -0,0 +1,3 @@ +Class: GeckoProfiler
On this page

GeckoProfiler

Manages the Gecko Profiler for profiling Firefox performance.

Classes

GeckoProfiler

Methods

(async) start() → {Promise.<void>}

Starts the Gecko Profiler.
Throws:
Throws an error if not running Firefox or if the configuration is not set for custom profiling.
Type
Error
Returns:
A promise that resolves when the profiler is started.
Type: 
Promise.<void>

(async) stop() → {Promise.<void>}

Stops the Gecko Profiler and processes the collected data.
Throws:
Throws an error if not running Firefox or if custom profiling was not started.
Type
Error
Returns:
A promise that resolves when the profiler is stopped and the data is processed.
Type: 
Promise.<void>
\ No newline at end of file diff --git a/docs/documentation/sitespeed.io/scripting/JavaScript.html b/docs/documentation/sitespeed.io/scripting/JavaScript.html new file mode 100644 index 0000000000..7f7792f9d8 --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/JavaScript.html @@ -0,0 +1,3 @@ +Class: JavaScript
On this page

JavaScript

Provides functionality to execute JavaScript code in the context of the current page.

Classes

JavaScript

Methods

(async) run(js) → {Promise.<*>}

Executes a JavaScript script.
Parameters:
NameTypeDescription
jsstringThe JavaScript code to execute.
Throws:
Throws an error if the JavaScript cannot be executed.
Type
Error
Returns:
A promise that resolves with the result of the executed script.
Type: 
Promise.<*>

(async) runAndWait(js) → {Promise.<*>}

Executes a JavaScript script and waits for the page complete check to finish.
Parameters:
NameTypeDescription
jsstringThe JavaScript code to execute.
Throws:
Throws an error if the JavaScript cannot be executed.
Type
Error
Returns:
A promise that resolves with the result of the executed script and the completion of the page load.
Type: 
Promise.<*>

(async) runPrivileged(js) → {Promise.<*>}

Executes synchronous privileged JavaScript.
Parameters:
NameTypeDescription
jsstringThe privileged JavaScript code to execute.
Throws:
Throws an error if the privileged JavaScript cannot be executed.
Type
Error
Returns:
A promise that resolves with the result of the executed privileged script.
Type: 
Promise.<*>

(async) runPrivilegedAndWait(js) → {Promise.<*>}

Executes synchronous privileged JavaScript and waits for the page complete check to finish.
Parameters:
NameTypeDescription
jsstringThe privileged JavaScript code to execute.
Throws:
Throws an error if the privileged JavaScript cannot be executed.
Type
Error
Returns:
A promise that resolves with the result of the executed privileged script and the completion of the page load.
Type: 
Promise.<*>

(async) runPrivilegedAsync(js) → {Promise.<*>}

Executes asynchronous privileged JavaScript.
Parameters:
NameTypeDescription
jsstringThe asynchronous privileged JavaScript code to execute.
Throws:
Throws an error if the asynchronous privileged JavaScript cannot be executed.
Type
Error
Returns:
A promise that resolves with the result of the executed asynchronous privileged script.
Type: 
Promise.<*>
\ No newline at end of file diff --git a/docs/documentation/sitespeed.io/scripting/Measure.html b/docs/documentation/sitespeed.io/scripting/Measure.html new file mode 100644 index 0000000000..247597996d --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/Measure.html @@ -0,0 +1,8 @@ +Class: Measure
On this page

Measure

A measurement tool for browser-based metrics, handling various aspects of metric collection including navigation, video recording, and data collection.

Classes

Measure

Methods

add(name, value)

Adds a custom metric to the current measurement result. This method should be called after a measurement has started and before it has stopped.
Parameters:
NameTypeDescription
namestringThe name of the metric.
value*The value of the metric.
Throws:
Throws an error if called before a measurement cycle has started.
Type
Error

addObject(object)

Adds multiple custom metrics to the current measurement result. This method accepts an object containing multiple key-value pairs representing different metrics. Similar to `add`, it should be used within an active measurement cycle.
Parameters:
NameTypeDescription
objectObjectAn object containing key-value pairs of metrics to add.
Throws:
Throws an error if called before a measurement cycle has started.
Type
Error

(async) start(urlOrAlias, optionalAliasopt) → {Promise.<void>}

Starts the measurement process for a given URL or an alias. It supports starting measurements by either directly providing a URL or using an alias. If a URL is provided, it navigates to that URL and performs the measurement. If an alias is provided, or no URL is available, it sets up the environment for a user-driven navigation.
Parameters:
NameTypeAttributesDescription
urlOrAliasstringThe URL to navigate to, or an alias representing the test.
optionalAliasstring<optional>
An optional alias that can be used if the first parameter is a URL.
Throws:
Throws an error if navigation fails or if there are issues in the setup process.
Type
Error
Returns:
A promise that resolves when the start process is complete, or rejects if there are errors.
Type: 
Promise.<void>
Example
await commands.measure.start('https://www.example.org');
+// Or start the measurement and click on a link
+await commands.measure.start();
+await commands.click.byLinkTextAndWait('Documentation');
+// Remember to stop the measurements if you do not provide a URL
+await commands.measure.stop();

(async) stop(testedStartUrl) → {Promise}

Stops the measurement process, collects metrics, and handles any post-measurement tasks. It finalizes the URL being tested, manages any URL-specific metadata, stops any ongoing video recordings, and initiates the data collection process.
Parameters:
NameTypeDescription
testedStartUrlstringThe URL that was initially tested. If not provided, it will be obtained from the browser.
Throws:
Throws an error if there are issues in stopping the measurement or collecting data.
Type
Error
Returns:
A promise that resolves with the collected metrics data.
Type: 
Promise
\ No newline at end of file diff --git a/docs/documentation/sitespeed.io/scripting/Meta.html b/docs/documentation/sitespeed.io/scripting/Meta.html new file mode 100644 index 0000000000..f04e0364d6 --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/Meta.html @@ -0,0 +1,3 @@ +Class: Meta
On this page

Meta

Add meta data to your user journey.

Classes

Meta

Methods

setDescription(text)

Sets the description for the user journey.
Parameters:
NameTypeDescription
textstringThe text to set as the description.
Example
commands.meta.setDescription('My test');

setTitle(text)

Sets the title for the user journey.
Parameters:
NameTypeDescription
textstringThe text to set as the title.
Example
commands.meta.setTitle('Test title');
\ No newline at end of file diff --git a/docs/documentation/sitespeed.io/scripting/MouseMove.html b/docs/documentation/sitespeed.io/scripting/MouseMove.html new file mode 100644 index 0000000000..19365085d6 --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/MouseMove.html @@ -0,0 +1,3 @@ +Class: MouseMove
On this page

MouseMove

Provides functionality to move the mouse cursor to elements or specific positions on a web page.

Classes

MouseMove

Methods

(async) byOffset(xOffset, yOffset) → {Promise.<void>}

Moves the mouse cursor by an offset from its current position.
Parameters:
NameTypeDescription
xOffsetnumberThe x offset to move by.
yOffsetnumberThe y offset to move by.
Throws:
Throws an error if the action cannot be performed.
Type
Error
Returns:
A promise that resolves when the mouse has moved by the specified offset.
Type: 
Promise.<void>

(async) bySelector(selector) → {Promise.<void>}

Moves the mouse cursor to an element that matches a given CSS selector.
Parameters:
NameTypeDescription
selectorstringThe CSS selector of the element to move to.
Throws:
Throws an error if the element is not found.
Type
Error
Returns:
A promise that resolves when the mouse has moved to the element.
Type: 
Promise.<void>

(async) byXpath(xpath) → {Promise.<void>}

Moves the mouse cursor to an element that matches a given XPath selector.
Parameters:
NameTypeDescription
xpathstringThe XPath selector of the element to move to.
Throws:
Throws an error if the element is not found.
Type
Error
Returns:
A promise that resolves when the mouse has moved to the element.
Type: 
Promise.<void>

(async) toPosition(xPos, yPos) → {Promise.<void>}

Moves the mouse cursor to a specific position on the screen.
Parameters:
NameTypeDescription
xPosnumberThe x-coordinate on the screen to move to.
yPosnumberThe y-coordinate on the screen to move to.
Throws:
Throws an error if the action cannot be performed.
Type
Error
Returns:
A promise that resolves when the mouse has moved to the specified position.
Type: 
Promise.<void>
\ No newline at end of file diff --git a/docs/documentation/sitespeed.io/scripting/Navigation.html b/docs/documentation/sitespeed.io/scripting/Navigation.html new file mode 100644 index 0000000000..c9345ec7b1 --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/Navigation.html @@ -0,0 +1,3 @@ +Class: Navigation
On this page

Navigation

Provides functionality to control browser navigation such as back, forward, and refresh actions.

Classes

Navigation

Methods

(async) back(optionsopt) → {Promise.<void>}

Navigates backward in the browser's history.
Parameters:
NameTypeAttributesDescription
optionsObject<optional>
Additional options for navigation. Set {wait:true} to wait for the page complete check to run.
Throws:
Throws an error if navigation fails.
Type
Error
Returns:
A promise that resolves when the navigation action is completed.
Type: 
Promise.<void>

(async) forward(optionsopt) → {Promise.<void>}

Navigates forward in the browser's history.
Parameters:
NameTypeAttributesDescription
optionsObject<optional>
Additional options for navigation. Set {wait:true} to wait for the page complete check to run.
Throws:
Throws an error if navigation fails.
Type
Error
Returns:
A promise that resolves when the navigation action is completed.
Type: 
Promise.<void>

(async) refresh(optionsopt) → {Promise.<void>}

Refreshes the current page.
Parameters:
NameTypeAttributesDescription
optionsObject<optional>
Additional options for refresh action. Set {wait:true} to wait for the page complete check to run.
Throws:
Throws an error if refreshing the page fails.
Type
Error
Returns:
A promise that resolves when the page has been refreshed.
Type: 
Promise.<void>
\ No newline at end of file diff --git a/docs/documentation/sitespeed.io/scripting/Screenshot.html b/docs/documentation/sitespeed.io/scripting/Screenshot.html new file mode 100644 index 0000000000..1c1ce27a97 --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/Screenshot.html @@ -0,0 +1,3 @@ +Class: Screenshot
On this page

Screenshot

Take a screenshot. The screenshot will be stored to disk, named by the name provided to the take function.

Classes

Screenshot

Methods

(async) take(name) → {Promise.<Object>}

Takes a screenshot and saves it using the screenshot manager.
Parameters:
NameTypeDescription
namestringThe name to assign to the screenshot file.
Throws:
Throws an error if the name parameter is not provided.
Type
Error
Returns:
A promise that resolves with the screenshot details.
Type: 
Promise.<Object>
Example
async commands.screenshot.take('my_startpage');
\ No newline at end of file diff --git a/docs/documentation/sitespeed.io/scripting/Scroll.html b/docs/documentation/sitespeed.io/scripting/Scroll.html new file mode 100644 index 0000000000..ee85df9798 --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/Scroll.html @@ -0,0 +1,3 @@ +Class: Scroll
On this page

Scroll

Provides functionality to control page scrolling in the browser.

Classes

Scroll

Methods

(async) byLines(lines) → {Promise.<void>}

Scrolls the page by the specified number of lines. This method is only supported in Firefox.
Parameters:
NameTypeDescription
linesnumberThe number of lines to scroll.
Throws:
Throws an error if not used in Firefox.
Type
Error
Returns:
A promise that resolves when the scrolling action is completed.
Type: 
Promise.<void>

(async) byPages(pages) → {Promise.<void>}

Scrolls the page by the specified number of pages.
Parameters:
NameTypeDescription
pagesnumberThe number of pages to scroll.
Returns:
A promise that resolves when the scrolling action is completed.
Type: 
Promise.<void>

(async) byPixels(Xpixels, Ypixels) → {Promise.<void>}

Scrolls the page by the specified number of pixels.
Parameters:
NameTypeDescription
XpixelsnumberThe number of pixels to scroll horizontally.
YpixelsnumberThe number of pixels to scroll vertically.
Returns:
A promise that resolves when the scrolling action is completed.
Type: 
Promise.<void>

(async) toBottom(delayTimeopt) → {Promise.<void>}

Scrolls to the bottom of the page, scrolling page by page with a delay between each scroll.
Parameters:
NameTypeAttributesDefaultDescription
delayTimenumber<optional>
250The delay time in milliseconds between each scroll.
Returns:
A promise that resolves when the scrolling to the bottom is completed.
Type: 
Promise.<void>
\ No newline at end of file diff --git a/docs/documentation/sitespeed.io/scripting/Select.html b/docs/documentation/sitespeed.io/scripting/Select.html new file mode 100644 index 0000000000..99dd7f5eae --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/Select.html @@ -0,0 +1,3 @@ +Class: Select
On this page

Select

Provides functionality to interact with `

Classes

Select

Methods

(async) deselectById(selectId) → {Promise.<void>}

Deselects all options in a `
Parameters:
NameTypeDescription
selectIdstringThe ID of the `
Throws:
Throws an error if the `
Type
Error
Returns:
A promise that resolves when all options are deselected.
Type: 
Promise.<void>

(async) getSelectedValueById(selectId) → {Promise.<string>}

Retrieves the value of the selected option in a `
Parameters:
NameTypeDescription
selectIdstringThe ID of the `
Throws:
Throws an error if the `
Type
Error
Returns:
A promise that resolves with the value of the selected option.
Type: 
Promise.<string>

(async) getValuesById(selectId) → {Promise.<Array.<string>>}

Retrieves all option values in a `
Parameters:
NameTypeDescription
selectIdstringThe ID of the `
Throws:
Throws an error if the `
Type
Error
Returns:
A promise that resolves with an array of the values of the options.
Type: 
Promise.<Array.<string>>

(async) selectByIdAndIndex(selectId, index) → {Promise.<void>}

Selects an option in a `
Parameters:
NameTypeDescription
selectIdstringThe ID of the `
indexnumberThe index of the option to select.
Throws:
Throws an error if the `
Type
Error
Returns:
A promise that resolves when the option is selected.
Type: 
Promise.<void>

(async) selectByIdAndValue(selectId, value) → {Promise.<void>}

Selects an option in a `
Parameters:
NameTypeDescription
selectIdstringThe ID of the `
valuestringThe value of the option to select.
Throws:
Throws an error if the `
Type
Error
Returns:
A promise that resolves when the option is selected.
Type: 
Promise.<void>

(async) selectByNameAndIndex(selectName, index) → {Promise.<void>}

Selects an option in a `
Parameters:
NameTypeDescription
selectNamestringThe name of the `
indexnumberThe index of the option to select.
Throws:
Throws an error if the `
Type
Error
Returns:
A promise that resolves when the option is selected.
Type: 
Promise.<void>

(async) selectByNameAndValue(selectName, value) → {Promise.<void>}

Selects an option in a `
Parameters:
NameTypeDescription
selectNamestringThe name of the `
valuestringThe value of the option to select.
Throws:
Throws an error if the `
Type
Error
Returns:
A promise that resolves when the option is selected.
Type: 
Promise.<void>
\ No newline at end of file diff --git a/docs/documentation/sitespeed.io/scripting/Set.html b/docs/documentation/sitespeed.io/scripting/Set.html new file mode 100644 index 0000000000..eb97fc6db9 --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/Set.html @@ -0,0 +1,3 @@ +Class: Set
On this page

Set

Provides functionality to set properties like innerHTML, innerText, and value on elements in a web page.

Classes

Set

Methods

(async) innerHtml(html, selector) → {Promise.<void>}

Sets the innerHTML of an element using a CSS selector.
Parameters:
NameTypeDescription
htmlstringThe HTML string to set as innerHTML.
selectorstringThe CSS selector of the element.
Throws:
Throws an error if the element is not found.
Type
Error
Returns:
A promise that resolves when the innerHTML is set.
Type: 
Promise.<void>

(async) innerHtmlById(html, id) → {Promise.<void>}

Sets the innerHTML of an element using its ID.
Parameters:
NameTypeDescription
htmlstringThe HTML string to set as innerHTML.
idstringThe ID of the element.
Throws:
Throws an error if the element is not found.
Type
Error
Returns:
A promise that resolves when the innerHTML is set.
Type: 
Promise.<void>

(async) innerText(text, selector) → {Promise.<void>}

Sets the innerText of an element using a CSS selector.
Parameters:
NameTypeDescription
textstringThe text to set as innerText.
selectorstringThe CSS selector of the element.
Throws:
Throws an error if the element is not found.
Type
Error
Returns:
A promise that resolves when the innerText is set.
Type: 
Promise.<void>

(async) innerTextById(text, id) → {Promise.<void>}

Sets the innerText of an element using its ID.
Parameters:
NameTypeDescription
textstringThe text to set as innerText.
idstringThe ID of the element.
Throws:
Throws an error if the element is not found.
Type
Error
Returns:
A promise that resolves when the innerText is set.
Type: 
Promise.<void>

(async) value(value, selector) → {Promise.<void>}

Sets the value of an element using a CSS selector.
Parameters:
NameTypeDescription
valuestringThe value to set on the element.
selectorstringThe CSS selector of the element.
Throws:
Throws an error if the element is not found.
Type
Error
Returns:
A promise that resolves when the value is set.
Type: 
Promise.<void>

(async) valueById(value, id) → {Promise.<void>}

Sets the value of an element using its ID.
Parameters:
NameTypeDescription
valuestringThe value to set on the element.
idstringThe ID of the element.
Throws:
Throws an error if the element is not found.
Type
Error
Returns:
A promise that resolves when the value is set.
Type: 
Promise.<void>
\ No newline at end of file diff --git a/docs/documentation/sitespeed.io/scripting/SingleClick.html b/docs/documentation/sitespeed.io/scripting/SingleClick.html new file mode 100644 index 0000000000..d393dc0e61 --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/SingleClick.html @@ -0,0 +1,3 @@ +Class: SingleClick
On this page

SingleClick

Provides functionality to perform a single click action on elements or at specific positions in a web page. Uses Seleniums Action API.

Classes

SingleClick

Methods

(async) atCursor(optionsopt) → {Promise.<void>}

Performs a single mouse click at the current cursor position.
Parameters:
NameTypeAttributesDescription
optionsObject<optional>
Additional options for the click action.
Throws:
Throws an error if the single click action cannot be performed.
Type
Error
Returns:
A promise that resolves when the single click occurs.
Type: 
Promise.<void>

(async) atCursorAndWait() → {Promise.<void>}

Performs a single mouse click at the current cursor position and waits on the page complete check.
Throws:
Throws an error if the single click action cannot be performed.
Type
Error
Returns:
A promise that resolves when the single click occurs.
Type: 
Promise.<void>

(async) byLinkText(text) → {Promise.<void>}

Clicks on a link whose visible text matches the given string.
Parameters:
NameTypeDescription
textstringThe visible text of the link to click.
Throws:
Throws an error if the link is not found.
Type
Error
Returns:
A promise that resolves when the click action is performed.
Type: 
Promise.<void>

(async) byLinkTextAndWait(text) → {Promise.<void>}

Clicks on a link whose visible text matches the given string and waits on the opage complete check.
Parameters:
NameTypeDescription
textstringThe visible text of the link to click.
Throws:
Throws an error if the link is not found.
Type
Error
Returns:
A promise that resolves when the click action is performed.
Type: 
Promise.<void>

(async) byPartialLinkText(text) → {Promise.<void>}

Clicks on a link whose visible text contains the given substring.
Parameters:
NameTypeDescription
textstringThe substring of the visible text of the link to click.
Throws:
Throws an error if the link is not found.
Type
Error
Returns:
A promise that resolves when the click action is performed.
Type: 
Promise.<void>

(async) byPartialLinkTextAndWait(text) → {Promise.<void>}

Clicks on a link whose visible text contains the given substring and waits on the page complete checl.
Parameters:
NameTypeDescription
textstringThe substring of the visible text of the link to click.
Throws:
Throws an error if the link is not found.
Type
Error
Returns:
A promise that resolves when the click action is performed.
Type: 
Promise.<void>

(async) bySelector(selector, optionsopt) → {Promise.<void>}

Performs a single mouse click on an element matching a given CSS selector.
Parameters:
NameTypeAttributesDescription
selectorstringThe CSS selector of the element to click.
optionsObject<optional>
Additional options for the click action.
Throws:
Throws an error if the element is not found.
Type
Error
Returns:
A promise that resolves when the single click action is performed.
Type: 
Promise.<void>

(async) bySelectorAndWait(selector) → {Promise.<void>}

Performs a single mouse click on an element matching a given CSS selector and waits on the page complete check.
Parameters:
NameTypeDescription
selectorstringThe CSS selector of the element to click.
Throws:
Throws an error if the element is not found.
Type
Error
Returns:
A promise that resolves when the single click action is performed.
Type: 
Promise.<void>

(async) byXpath(xpath, optionsopt) → {Promise.<void>}

Performs a single mouse click on an element matching a given XPath selector.
Parameters:
NameTypeAttributesDescription
xpathstringThe XPath selector of the element to click.
optionsObject<optional>
Additional options for the click action.
Throws:
Throws an error if the element is not found.
Type
Error
Returns:
A promise that resolves when the single click action is performed.
Type: 
Promise.<void>

(async) byXpathAndWait(xpath) → {Promise.<void>}

Performs a single mouse click on an element matching a given XPath selector and wait for page complete check.
Parameters:
NameTypeDescription
xpathstringThe XPath selector of the element to click.
Throws:
Throws an error if the element is not found.
Type
Error
Returns:
A promise that resolves when the single click action is performed.
Type: 
Promise.<void>
\ No newline at end of file diff --git a/docs/documentation/sitespeed.io/scripting/StopWatch.html b/docs/documentation/sitespeed.io/scripting/StopWatch.html new file mode 100644 index 0000000000..78d10d4165 --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/StopWatch.html @@ -0,0 +1,3 @@ +Class: StopWatch
On this page

StopWatch

A stopwatch utility for measuring time intervals.

Classes

StopWatch

Methods

getName() → {string}

Gets the name of the stopwatch.
Returns:
The name of the stopwatch.
Type: 
string

start()

Starts the stopwatch.

stop() → {number}

Stops the stopwatch.
Returns:
The measured time in milliseconds.
Type: 
number

stopAndAdd() → {number}

Stops the stopwatch and automatically adds the measured time to the last measured page. Logs an error if no page has been measured.
Returns:
The measured time in milliseconds.
Type: 
number
\ No newline at end of file diff --git a/docs/documentation/sitespeed.io/scripting/Switch.html b/docs/documentation/sitespeed.io/scripting/Switch.html new file mode 100644 index 0000000000..2505ebd1c7 --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/Switch.html @@ -0,0 +1,3 @@ +Class: Switch
On this page

Switch

Provides functionality to switch between frames, windows, and tabs in the browser.

Classes

Switch

Methods

(async) toFrame(id)

Switches to a frame identified by its ID.
Parameters:
NameTypeDescription
idstring | numberThe ID of the frame.
Throws:
Throws an error if switching to the frame fails.
Type
Error

(async) toFrameBySelector(selector)

Switches to a frame identified by a CSS selector.
Parameters:
NameTypeDescription
selectorstringThe CSS selector of the frame element.
Throws:
Throws an error if the frame is not found or switching fails.
Type
Error

(async) toFrameByXpath(xpath)

Switches to a frame identified by an XPath.
Parameters:
NameTypeDescription
xpathstringThe XPath of the frame element.
Throws:
Throws an error if the frame is not found or switching fails.
Type
Error

(async) toNewTab(urlopt)

Opens a new tab and optionally navigates to a URL.
Parameters:
NameTypeAttributesDescription
urlstring<optional>
Optional URL to navigate to in the new tab.
Throws:
Throws an error if opening a new tab fails.
Type
Error

(async) toNewWindow(urlopt)

Opens a new window and optionally navigates to a URL.
Parameters:
NameTypeAttributesDescription
urlstring<optional>
Optional URL to navigate to in the new window.
Throws:
Throws an error if opening a new window fails.
Type
Error

(async) toParentFrame()

Switches to the parent frame of the current frame.
Throws:
Throws an error if switching to the parent frame fails.
Type
Error

(async) toWindow(name)

Switches to a window identified by its name.
Parameters:
NameTypeDescription
namestringThe name of the window.
Throws:
Throws an error if switching to the window fails.
Type
Error
\ No newline at end of file diff --git a/docs/documentation/sitespeed.io/scripting/Wait.html b/docs/documentation/sitespeed.io/scripting/Wait.html new file mode 100644 index 0000000000..5ddefd9728 --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/Wait.html @@ -0,0 +1,3 @@ +Class: Wait
On this page

Wait

Provides functionality to wait for different conditions in the browser.

Classes

Wait

Methods

(async) byCondition(jsExpression, maxTime) → {Promise.<void>}

Waits for a JavaScript condition to return a truthy value within a maximum time.
Parameters:
NameTypeDescription
jsExpressionstringThe JavaScript expression to evaluate.
maxTimenumberMaximum time to wait in milliseconds.
Throws:
Throws an error if the condition is not met within the specified time.
Type
Error
Returns:
A promise that resolves when the condition becomes truthy or the time times out.
Type: 
Promise.<void>

(async) byId(id, maxTime) → {Promise.<void>}

Waits for an element with a specific ID to appear within a maximum time.
Parameters:
NameTypeDescription
idstringThe ID of the element to wait for.
maxTimenumberMaximum time to wait in milliseconds.
Throws:
Throws an error if the element is not found within the specified time.
Type
Error
Returns:
A promise that resolves when the element is found or the time times out.
Type: 
Promise.<void>

(async) byPageToComplete() → {Promise.<void>}

Waits for the page to finish loading.
Returns:
A promise that resolves when the page complete check has finished.
Type: 
Promise.<void>
Example
async commands.wait.byPageToComplete();

(async) bySelector(selector, maxTime) → {Promise.<void>}

Waits for an element located by a CSS selector to appear within a maximum time.
Parameters:
NameTypeDescription
selectorstringThe CSS selector of the element to wait for.
maxTimenumberMaximum time to wait in milliseconds.
Throws:
Throws an error if the element is not found within the specified time.
Type
Error
Returns:
A promise that resolves when the element is found or the time times out.
Type: 
Promise.<void>

(async) byTime(ms) → {Promise.<void>}

Waits for a specified amount of time.
Parameters:
NameTypeDescription
msnumberThe time in milliseconds to wait.
Returns:
A promise that resolves when the specified time has elapsed.
Type: 
Promise.<void>
Example
async commands.wait.byTime(1000);

(async) byXpath(xpath, maxTime) → {Promise.<void>}

Waits for an element located by XPath to appear within a maximum time.
Parameters:
NameTypeDescription
xpathstringThe XPath of the element to wait for.
maxTimenumberMaximum time to wait in milliseconds.
Throws:
Throws an error if the element is not found within the specified time.
Type
Error
Returns:
A promise that resolves when the element is found or the time times out.
Type: 
Promise.<void>
\ No newline at end of file diff --git a/docs/documentation/sitespeed.io/scripting/data/search.json b/docs/documentation/sitespeed.io/scripting/data/search.json new file mode 100644 index 0000000000..c265d64dd5 --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/data/search.json @@ -0,0 +1 @@ +{"list":[{"title":"Actions","link":"Actions"},{"title":"Actions#getActions","link":"getActions","description":"Retrieves the current action sequence builder.\nThe actions builder can be used to chain multiple browser actions."},{"title":"AddText","link":"AddText"},{"title":"AddText#byClassName","link":"byClassName","description":"Adds text to an element identified by its class name."},{"title":"AddText#byId","link":"byId","description":"Adds text to an element identified by its ID."},{"title":"AddText#byName","link":"byName","description":"Adds text to an element identified by its name attribute."},{"title":"AddText#bySelector","link":"bySelector","description":"Adds text to an element identified by its CSS selector."},{"title":"AddText#byXpath","link":"byXpath","description":"Adds text to an element identified by its XPath."},{"title":"AndroidCommand","link":"AndroidCommand"},{"title":"AndroidCommand#shell","link":"shell","description":"Runs a shell command on the connected Android device.\nThis method requires the Android device to be properly configured."},{"title":"AndroidCommand#shellAsRoot","link":"shellAsRoot","description":"Runs a shell command on the connected Android device as the root user.\nThis method requires the Android device to be properly configured and that you\nrooted the device."},{"title":"Cache","link":"Cache"},{"title":"Cache#clear","link":"clear","description":"Clears the browser cache. This includes both cache and cookies.\n\nFor Firefox, it uses the extensionServer setup with specific options.\nFor Chrome and Edge, it uses the Chrome DevTools Protocol (CDP) commands.\nIf the browser is not supported, logs an error message."},{"title":"Cache#clearKeepCookies","link":"clearKeepCookies","description":"Clears the browser cache while keeping the cookies.\n\nFor Firefox, it uses the extensionServer setup with specific options.\nFor Chrome and Edge, it uses the Chrome DevTools Protocol (CDP) command to clear the cache.\nIf the browser is not supported, logs an error message."},{"title":"ChromeDevelopmentToolsProtocol","link":"ChromeDevelopmentToolsProtocol"},{"title":"ChromeDevelopmentToolsProtocol#getRawClient","link":"getRawClient","description":"Retrieves the raw client for the DevTools Protocol."},{"title":"ChromeDevelopmentToolsProtocol#on","link":"on","description":"Sets up an event listener for a specific DevTools Protocol event."},{"title":"ChromeDevelopmentToolsProtocol#send","link":"send","description":"Sends a command to the DevTools Protocol."},{"title":"ChromeDevelopmentToolsProtocol#sendAndGet","link":"sendAndGet","description":"Sends a command to the DevTools Protocol and returns the result."},{"title":"ChromeTrace","link":"ChromeTrace"},{"title":"ChromeTrace#start","link":"start","description":"Starts the Chrome trace collection."},{"title":"ChromeTrace#stop","link":"stop","description":"Stops the Chrome trace collection, processes the collected data, and attaches it to the result object."},{"title":"Click","link":"Click"},{"title":"Click#byClassName","link":"byClassName","description":"Clicks on an element identified by its class name."},{"title":"Click#byClassNameAndWait","link":"byClassNameAndWait","description":"Clicks on an element identified by its class name and waits for the page complete check to finish."},{"title":"Click#byId","link":"byId","description":"Clicks on an element located by its ID."},{"title":"Click#byIdAndWait","link":"byIdAndWait","description":"Click on link located by the ID attribute. Uses document.getElementById() to find the element. And wait for page complete check to finish."},{"title":"Click#byJs","link":"byJs","description":"Clicks on an element located by evaluating a JavaScript expression."},{"title":"Click#byJsAndWait","link":"byJsAndWait","description":"Clicks on an element located by evaluating a JavaScript expression and waits for the page complete check to finish."},{"title":"Click#byLinkText","link":"byLinkText","description":"Clicks on a link whose visible text matches the given string."},{"title":"Click#byLinkTextAndWait","link":"byLinkTextAndWait","description":"Clicks on a link whose visible text matches the given string and waits for the page complete check to finish."},{"title":"Click#byName","link":"byName","description":"Clicks on an element located by its name attribute."},{"title":"Click#byPartialLinkText","link":"byPartialLinkText","description":"Clicks on a link whose visible text contains the given substring."},{"title":"Click#byPartialLinkTextAndWait","link":"byPartialLinkTextAndWait","description":"Clicks on a link whose visible text contains the given substring and waits for the page complete check to finish."},{"title":"Click#bySelector","link":"bySelector","description":"Clicks on an element located by its CSS selector."},{"title":"Click#bySelectorAndWait","link":"bySelectorAndWait","description":"Clicks on an element located by its CSS selector and waits for the page complete check to finish."},{"title":"Click#byXpath","link":"byXpath","description":"Clicks on an element that matches a given XPath selector."},{"title":"Click#byXpathAndWait","link":"byXpathAndWait","description":"Clicks on an element that matches a given XPath selector and waits for the page complete check to finish."},{"title":"ClickAndHold","link":"ClickAndHold"},{"title":"ClickAndHold#atCursor","link":"atCursor","description":"Clicks and holds at the current cursor position."},{"title":"ClickAndHold#atPosition","link":"atPosition","description":"Clicks and holds at the specified screen coordinates."},{"title":"ClickAndHold#bySelector","link":"bySelector","description":"Clicks and holds on an element that matches a given CSS selector."},{"title":"ClickAndHold#byXpath","link":"byXpath","description":"Clicks and holds on an element that matches a given XPath selector."},{"title":"ClickAndHold#releaseAtPosition","link":"releaseAtPosition","description":"Releases the mouse button at the specified screen coordinates."},{"title":"ClickAndHold#releaseAtSelector","link":"releaseAtSelector","description":"Releases the mouse button on an element matching the specified CSS selector."},{"title":"ClickAndHold#releaseAtXpath","link":"releaseAtXpath","description":"Releases the mouse button on an element matching the specified XPath."},{"title":"Commands","link":"Commands"},{"title":"Commands#action","link":"action","description":"Selenium's action sequence functionality."},{"title":"Commands#addText","link":"addText","description":"Provides functionality to add text to elements on a web page using various selectors."},{"title":"Commands#android","link":"android","description":"Provides commands for interacting with an Android device."},{"title":"Commands#cache","link":"cache","description":"Manages the browser's cache."},{"title":"Commands#cdp","link":"cdp","description":"Use the Chrome DevTools Protocol, available in Chrome and Edge."},{"title":"Commands#click","link":"click","description":"Provides functionality to perform click actions on elements in a web page using various selectors."},{"title":"Commands#debug","link":"debug","description":"Provides debugging capabilities within a browser automation script.\nIt allows setting breakpoints to pause script execution and inspect the current state."},{"title":"Commands#element","link":"element","description":"Get Selenium's WebElements."},{"title":"Commands#error","link":"error","description":"Add a text that will be an error attached to the current page."},{"title":"Commands#js","link":"js","description":"Executes JavaScript in the browser context."},{"title":"Commands#markAsFailure","link":"markAsFailure","description":"Mark this run as an failure. Add a message that explains the failure."},{"title":"Commands#measure","link":"measure","description":"Provides functionality for measuring a navigation."},{"title":"Commands#meta","link":"meta","description":"Adds metadata to the user journey."},{"title":"Commands#mouse","link":"mouse","description":"Interact with the page using the mouse."},{"title":"Commands#mouse.clickAndHold","link":"clickAndHold","description":"Provides functionality to click and hold elements on a web page using different strategies."},{"title":"Commands#mouse.contextClick","link":"contextClick","description":"Perform a context click (right-click) on elements in a web page."},{"title":"Commands#mouse.doubleClick","link":"doubleClick","description":"Provides functionality to perform a double-click action on elements in a web page."},{"title":"Commands#mouse.moveTo","link":"moveTo","description":"Move the mouse cursor to elements or specific positions on a web page."},{"title":"Commands#mouse.singleClick","link":"singleClick","description":"Provides functionality to perform a single click action on elements or at specific positions in a web page."},{"title":"Commands#navigate","link":"navigate","description":"Navigates to a specified URL and handles additional setup for a page visit."},{"title":"Commands#navigation","link":"navigation","description":"Provides functionality to control browser navigation such as back, forward, and refresh actions."},{"title":"Commands#screenshot","link":"screenshot","description":"Takes and manages screenshots."},{"title":"Commands#scroll","link":"scroll","description":"Provides functionality to control page scrolling in the browser."},{"title":"Commands#select","link":"select","description":"Interact with a select element."},{"title":"Commands#set","link":"set","description":"Sets values on HTML elements in the page."},{"title":"Commands#stopWatch","link":"stopWatch","description":"Stopwatch utility for measuring time intervals."},{"title":"Commands#switch","link":"switch","description":"Switches context to different frames, windows, or tabs in the browser."},{"title":"Commands#trace","link":"trace","description":"Manages Chrome trace functionality, enabling custom profiling and trace collection in Chrome."},{"title":"Commands#wait","link":"wait","description":"Provides functionality to wait for different conditions in the browser."},{"title":"ContextClick","link":"ContextClick"},{"title":"ContextClick#atCursor","link":"atCursor","description":"Performs a context click (right-click) at the current cursor position."},{"title":"ContextClick#bySelector","link":"bySelector","description":"Performs a context click (right-click) on an element that matches a given CSS selector."},{"title":"ContextClick#byXpath","link":"byXpath","description":"Performs a context click (right-click) on an element that matches a given XPath selector."},{"title":"Debug","link":"Debug"},{"title":"Debug#breakpoint","link":"breakpoint","description":"Adds a breakpoint to the script. The browser will pause at the breakpoint, waiting for user input to continue.\nThis is useful for debugging and inspecting the browser state at a specific point in the script."},{"title":"DoubleClick","link":"DoubleClick"},{"title":"DoubleClick#atCursor","link":"atCursor","description":"Performs a mouse double-click at the current cursor position."},{"title":"DoubleClick#bySelector","link":"bySelector","description":"Performs a mouse double-click on an element matching a given CSS selector."},{"title":"DoubleClick#byXpath","link":"byXpath","description":"Performs a mouse double-click on an element matching a given XPath selector."},{"title":"Element","link":"Element"},{"title":"Element#getByClassName","link":"getByClassName","description":"Finds an element by its class name."},{"title":"Element#getByCss","link":"getByCss","description":"Finds an element by its CSS selector."},{"title":"Element#getById","link":"getById","description":"Finds an element by its ID."},{"title":"Element#getByName","link":"getByName","description":"Finds an element by its name attribute."},{"title":"Element#getByXpath","link":"getByXpath","description":"Finds an element by its XPath."},{"title":"GeckoProfiler","link":"GeckoProfiler"},{"title":"GeckoProfiler#start","link":"start","description":"Starts the Gecko Profiler."},{"title":"GeckoProfiler#stop","link":"stop","description":"Stops the Gecko Profiler and processes the collected data."},{"title":"JavaScript","link":"JavaScript"},{"title":"JavaScript#run","link":"run","description":"Executes a JavaScript script."},{"title":"JavaScript#runAndWait","link":"runAndWait","description":"Executes a JavaScript script and waits for the page complete check to finish."},{"title":"JavaScript#runPrivileged","link":"runPrivileged","description":"Executes synchronous privileged JavaScript."},{"title":"JavaScript#runPrivilegedAndWait","link":"runPrivilegedAndWait","description":"Executes synchronous privileged JavaScript and waits for the page complete check to finish."},{"title":"JavaScript#runPrivilegedAsync","link":"runPrivilegedAsync","description":"Executes asynchronous privileged JavaScript."},{"title":"Measure","link":"Measure"},{"title":"Measure#add","link":"add","description":"Adds a custom metric to the current measurement result.\nThis method should be called after a measurement has started and before it has stopped."},{"title":"Measure#addObject","link":"addObject","description":"Adds multiple custom metrics to the current measurement result.\nThis method accepts an object containing multiple key-value pairs representing different metrics.\nSimilar to `add`, it should be used within an active measurement cycle."},{"title":"Measure#start","link":"start","description":"Starts the measurement process for a given URL or an alias.\n\nIt supports starting measurements by either directly providing a URL or using an alias.\nIf a URL is provided, it navigates to that URL and performs the measurement.\nIf an alias is provided, or no URL is available, it sets up the environment for a user-driven navigation."},{"title":"Measure#stop","link":"stop","description":"Stops the measurement process, collects metrics, and handles any post-measurement tasks.\nIt finalizes the URL being tested, manages any URL-specific metadata, stops any ongoing video recordings,\nand initiates the data collection process."},{"title":"Meta","link":"Meta"},{"title":"Meta#setDescription","link":"setDescription","description":"Sets the description for the user journey."},{"title":"Meta#setTitle","link":"setTitle","description":"Sets the title for the user journey."},{"title":"MouseMove","link":"MouseMove"},{"title":"MouseMove#byOffset","link":"byOffset","description":"Moves the mouse cursor by an offset from its current position."},{"title":"MouseMove#bySelector","link":"bySelector","description":"Moves the mouse cursor to an element that matches a given CSS selector."},{"title":"MouseMove#byXpath","link":"byXpath","description":"Moves the mouse cursor to an element that matches a given XPath selector."},{"title":"MouseMove#toPosition","link":"toPosition","description":"Moves the mouse cursor to a specific position on the screen."},{"title":"Navigation","link":"Navigation"},{"title":"Navigation#back","link":"back","description":"Navigates backward in the browser's history."},{"title":"Navigation#forward","link":"forward","description":"Navigates forward in the browser's history."},{"title":"Navigation#refresh","link":"refresh","description":"Refreshes the current page."},{"title":"Screenshot","link":"Screenshot"},{"title":"Screenshot#take","link":"take","description":"Takes a screenshot and saves it using the screenshot manager."},{"title":"Scroll","link":"Scroll"},{"title":"Scroll#byLines","link":"byLines","description":"Scrolls the page by the specified number of lines. This method is only supported in Firefox."},{"title":"Scroll#byPages","link":"byPages","description":"Scrolls the page by the specified number of pages."},{"title":"Scroll#byPixels","link":"byPixels","description":"Scrolls the page by the specified number of pixels."},{"title":"Scroll#toBottom","link":"toBottom","description":"Scrolls to the bottom of the page, scrolling page by page with a delay between each scroll."},{"title":"Select","link":"Select"},{"title":"Select#deselectById","link":"deselectById","description":"Deselects all options in a `` element by its ID."},{"title":"Select#getValuesById","link":"getValuesById","description":"Retrieves all option values in a `` element by its ID and the index of the option."},{"title":"Select#selectByIdAndValue","link":"selectByIdAndValue","description":"Selects an option in a `` element by its name and the index of the option."},{"title":"Select#selectByNameAndValue","link":"selectByNameAndValue","description":"Selects an option in a `
Type anything to view search result
\ No newline at end of file diff --git a/docs/documentation/sitespeed.io/scripting/index.md b/docs/documentation/sitespeed.io/scripting/index.md deleted file mode 100644 index 2de29e4daf..0000000000 --- a/docs/documentation/sitespeed.io/scripting/index.md +++ /dev/null @@ -1,1655 +0,0 @@ ---- -layout: default -title: Use scripts in sitespeed.io to measure a user journey. -description: With scripts you can simulate a user visiting to multiple pages, clicking on links, log in, adding items to the cart ... almost measure whatever you want! -keywords: selenium, web performance, sitespeed.io -nav: documentation -category: sitespeed.io -image: https://www.sitespeed.io/img/sitespeed-2.0-twitter.png -twitterdescription: Use scripts in sitespeed.io to measure a user journey. ---- -[Documentation]({{site.baseurl}}/documentation/sitespeed.io/) / Scripting - -# Scripting -{:.no_toc} - -Scripting in sitespeed.io allows you to measure user journeys by interacting with web pages. You create scripts in NodeJS that utilize context for accessing options and commands for webpage interactions like clicks, navigation, and starting/stopping measurements. Run your script with sitespeed.io to gather performance data. This feature is powerful for simulating real-user interactions and collecting performance metrics for complex workflows. - -Scripting work the same in Browsertime and sitespeed.io, the documentation here are for both of the tools. - -In sitespeed.io 27.0 the project was moved to a [pure ESM package](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c). You can choose to either have your scripting file be a ESM or CommonJS file. If you use ESM your file should end with *.mjs* . If it is common JS use *.cjs*. All our examples on this page is ESM. Before 27.0 all files was common JS. - -The user journey - -# Index -{:.no_toc} - -* Lets place the TOC here -{:toc} - -## Scripting basics - -### Simple script - -Start by creating a script file, say *measure.mjs*, with the following content: - -~~~javascript -export default async function (context, commands) { - return commands.measure.start('https://www.sitespeed.io'); -}; -~~~ - -Run your script with the command: ```sitespeed.io -n 1 --multi measure.mjs```. - -This script will measure the performance of the specified URL. For more advanced scripting options like handling clicks, navigation, and other interactions, scroll down​. - -### Details - -In scripting with Sitespeed.io, you have three options: - -1. Use **commands objects** which simplify script creation by wrapping plain JavaScript. For complex tasks, you might need to use plain JavaScript. -2. Execute plain JavaScript using the command `js.run()`, ideal for reusing code snippets from your browser's console. -3. If you are familiar with Selenium, you can use it directly in sitespeed.io. - -Regardless of the approach, use the **measure command** for accurate metric collection. Every script gets two objects: `context` for accessing options and utilities, and `commands` for interacting with webpages, like navigating and measuring. - -#### Context - -The context object are passed on to your function and give you access to the following: - -* *options* - All the options sent from the CLI to Browsertime. -* *log* - an instance to the log system so you can log from your navigation script. -* *index* - the index of the runs, so you can keep track of which run you are currently on. -* *storageManager* - The Browsertime storage manager that can help you read/store files to disk. -* *selenium.webdriver* - The Selenium [WebDriver public API object](https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/index.html). -* *selenium.driver* - The [instantiated version of the WebDriver](https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_WebDriver.html) driving the current version of the browser. - -In the code you can use the context like this: - -~~~javascript -export default async function (context, commands) { - // - context.log.info( - `My script is starting, you passed on ${context.options.iterations} iterations` - ); - context.log.info(`Iteration number ${context.index}`); - ... -} -~~~ - -If you want to pass data between your scripts you can do that with the context object. Here's an example of the first script: - -~~~javascript -export default async function (context, commands) { - // First you do what you need to do ... - // then just add a field to the context - context.myId = 15; -} -~~~ - -Then in your next script you can get that id: - -~~~javascript -export default async function (context, commands) { - const idToUse = context.myId; -} -~~~ - - -#### Commands -Commands are an help object that helps you interacting with webpages, like navigating and measuring. You can see the full list of commands [here]() and here's a short list of some of the most important one. - -* *[navigate(URL)](#navigateurl)* - Use this if you want to use the exact way as Browsertime navigates to a new URL (same settings with pageCompleteCheck etc). Note: the URL will not be measured automatically. -* *[measure.start(URL)](#measurestarturl)* - Start measuring and navigate to a new page in one go. -* *[measure.start(URL,alias)](#measurestarturl-alias)* - Start measuring and navigate to a new page in one go, while register an alias for that URL. -* *[measure.start()](#measurestart)* - Use this when you want to start to measure a page. This will start the video and prepare everything to collect metrics. Note: it will not navigate to the URL. -* *[measure.start(alias)](#measurestartalias)* - Use this when you want to start to measure a page. This will start the video and prepare everything to collect metrics. Note: it will not navigate to the URL and the next URL that will be accessed will get the alias. -* *[measure.stop()](#measurestop)* - Collect metrics for a page. -* *[timer.start()]()* Start a timer and measure the time. -* *[timer.stopAndAdd()]()* Stop the timer and add the result to the last tested URL. - -And then you have a few help commands: -* *[wait](#wait)* on a id to appear or wait x amount of ms. -* *[click](#click)* on a link and/or wait for the next page to load. -* *[js](#run-javascript)* - run JavaScript in the browser. -* *[switch](#switch)* to another frame or window. -* *[set](#set)* innerHtml, innerText or value to an element. - -Scripting only works for Browsertime. It will not work with Lighthouse/Google Pagespeed Insights or WebPageTest. -{: .note .note-info} - -### Await, return and promises. -If you are new to NodeJS using promises/await can be confusing, what does it mean and when should you use it? - -Some of the commands/function in Browsertime/sitespeed.io are asynchronous. This means that they return a promise. A promise is an action which will either be completed or rejected. It could be navigating to a new page, running JavaScript or waiting for an element to appear. - -To make sure your script wait on the action to complete, you use the `await` keyword. - -Navigating to sitespeed.io homepage and waiting on the navigation to complete: - -~~~javascript -export default async function (context, commands) { - await commands.navigate('https://www.sitespeed.io'); - // We will get here when the action is finished since we use await -} -~~~ - - -sitespeed.io/Browsertime gives full control to your script and is waiting for it to return a promise. That means that if you do many async functions/commands in your page, you should make sure you return the last promise back to sitespeed.io/Browsertime. That way it will wait until everything in your script has finished. Checkout this examample where we test two pages, we wait for the first to finish and then return the last promise back. - -~~~javascript -export default async function (context, commands) { - await commands.measure.start('https://www.sitespeed.io'); - return commands.measure.start('https://www.sitespeed.io/documentation/'); -} -~~~ - - -### Run -Run your script by passing it to sitespeed.io and adding the parameter ```--multi```. - -~~~bash -docker run --rm -v "$(pwd):/sitespeed.io" sitespeedio/sitespeed.io:{% include version/sitespeed.io.txt %} script.js --multi -~~~ - -#### Using multiple scripts - -If you have multiple scripts, you can just pass them in as well. - -~~~bash -docker run --rm -v "$(pwd):/sitespeed.io" sitespeedio/sitespeed.io:{% include version/sitespeed.io.txt %} script.mjs script2.mjs script3.mjs --multi -~~~ - -That way you can just split your long scripts into multiple files and make it easier to manage. In this example sitespeed.io will for each iteratiomn invoke *script.mjs* then *script2.mjs* and last *script3.mjs* - -#### Reuse scripts -You can break out code in multiple files. - -*test.mjs* -~~~javascript -import { example } from './exampleInclude.mjs'; -export default async function (context, commands) { - example(); -} -~~~ - -*exampleInclude.mjs* -~~~javascript -export async function example() { - console.log('This is my example function'); -} - -~~~ - -And then run it: -```sitespeed.io --multi test.mjs``` - - -#### Using setUp and tearDown in the same script - -This is a feature used by Mozilla and was created years ago. Nowadays you can probably just do everything in one script. - -Scripts can also directly define the ```--preScript``` and ```--postScript``` options by implementing a *setUp* and/or a *tearDown* function. These functions will get the same arguments than the test itself. When using this form, the three functions are declared in *module.exports* under the *setUp*, *tearDown* and *test* keys. This works for commons JS files. - -Here's a minimal example: - -~~~javascript -async function setUp(context, commands) { - // do some useful set up -}; - -async function perfTest(context, commands) { - // add your own code here -}; - -async function tearDown(context, commands) { - // do some cleanup here -}; - -module.exports = { - setUp: setUp, - tearDown: tearDown, - test: perfTest -}; -~~~ - - -## Measure - -In sitespeed.io, measurements can be conducted using a meassure command for page navigation performance, gathering various metrics during page load. Alternatively, the StopWatch command is used for measuring arbitrary durations, such as the time taken for certain actions to complete. When you create your script you need to know what you want to measure. - - -### The measure command - -The measure is used for preparing and measuring the navigation to a new URL, it starts the video recording, clears internal metrics, and collects technical metrics from the browser once the measurement is stopped​. - -In web performance measurement, a *"navigation"* refers to the process of moving from one URL to another, which triggers a series of events in the browser. This includes the unloading of the current page, fetching and executing necessary resources for the new page, rendering the new page, and completing the loading of the new page. The `commands.measure` in sitespeed.io is designed to measure the performance metrics of such navigations, capturing data like page load time, resource timings, and other relevant metrics from the start to the end of the navigation process. - -~~~javascript -export default async function (context, commands) { - // Navigate to https://www.sitespeed.io and measure the navigation - return commands.measure.start('https://www.sitespeed.io'); -}; -~~~ - -If you do not give the measure command a URL, the command will prepare everything and start the video. Then it’s up to you to navigate/click on a link/submit the page. You also need to stop the measurement so that Browsertime/sitespeed.io knows that you want the metrics. - -Here's an example where we measure navigating to the sitespeed.io documentation page by first navigation to the sitespeed.io start page and then clicking on a link. - -~~~javascript -export default async function (context, commands) { - - await commands.navigate('https://www.sitespeed.io'); - - await commands.measure.start('Documentation'); - // Using a xxxAndWait command will make the page wait for a navigation - await commands.click.byLinkTextAndWait('Documentation'); - return commands.measure.stop(); -} -~~~ - -### The stop watch command -The Stop Watch command is utilized when there's a need to measure something that is not a navigation, like the time taken for certain processes or actions. It's more manual where you start and stop the watch to measure the elapsed time​​. - -The stop watch metric will be automatically added to the page that was measured before the stop watch. - -~~~javascript -export default async function (context, commands) { - const stopWatch = commands.stopWatch.get('Before_navigating_page'); - // Do the thing you want to measure ... - // Then stop the watch - const time = stopWatch.stop(); - // Measure navigation to a page - await commands.measure.start('https://www.sitespeed.io'); - // Then attach that timing to that page. - commands.measure.add(stopWatch.getName(), time); -} -~~~ - -### Using user timings and element timings API -If you are in control of the page you are testing you can (and should) use the [User Timing API](https://developer.mozilla.org/en-US/docs/Web/API/Performance_API/User_timing) and the [Element Timing API](https://wicg.github.io/element-timing/). - -These are JavaScript APIs built into the browser. Almost all browser supports the User Timing API and Chrome(ium) browsers support the Element Timing API. - -Browsertime/sitespeed.io will automatically pick up those metrics when you run the measure command. - -You can also get those metrics running the JavsScript command. That is useful if you want to collect these metrics yourself and they happen after a page navigation. - -~~~javascript -export default async function (context, commands) { - await commands.navigate('https://www.sitespeed.io'); - - // The sitespeed.io start page has a user timing mark named userTimingHeader - const userTimingHeader = await commands.js.run( - `return performance.getEntriesByName('userTimingHeader')[0].startTime;` - ); - - // The sitespeed.io start page has a element timing api for the logo - const logoRenderTime = await commands.js.run(` - const observer = new PerformanceObserver(list => {}); - observer.observe({ type: 'element', buffered: true }); - const entries = observer.takeRecords(); - for (let entry of entries) { - if (entry.identifier === 'logo') { - return Number(entry.renderTime.toFixed(0)); - } - } - `); - - context.log.info( - `User Timing header: ${userTimingHeader} ms and Logo Element render time ${logoRenderTime} ms` - ); -} -~~~ - -### Measuring SPA -At the moment browser metrics like paint metrics that you can collect from JavaScript aren't updated when you are using a SPA/a soft navigation. There are [work beeing done here](https://github.com/w3c/performance-timeline/issues/168) and [here](https://developer.chrome.com/blog/soft-navigations-experiment/) in this area and we will work to use that in upcoming releases. - -In the current release, you need to record a video of the screen and use the visual metrics. Combine that with User Timings and Element timings and you can measure the most things. We are gonna update the documentation when we have a implementation working for soft navigations. - - -## Finding the right element - -One of the key things in your script is to be able to find the right element to invoke. If the element has an id it's easy. If not you can use developer tools in your favourite browser. The all work mostly the same: Open DevTools in the page you want to inspect, click on the element and right click on DevTools for that element. Then you will see something like this: - -![Using Safari to find the selector]({{site.baseurl}}/img/selector-safari.png){:loading="lazy"} -{: .img-thumbnail-center} -

- Using Safari to find the CSS Selector to the element -

- -![Using Firefox to find the selector]({{site.baseurl}}/img/selector-firefox.png){:loading="lazy"} -{: .img-thumbnail-center} -

- Using Firefox to find the CSS Selector to the element -

- -![Using Chrome to find the selector]({{site.baseurl}}/img/selector-chrome.png){:loading="lazy"} -{: .img-thumbnail-center} -

- Using Chrome to find the CSS Selector to the element -

- -## Debug -There's a couple of way that makes it easier to debug your scripts: - -* Run in `--debug` mode and add breakpoints to your code. The browser will open with devtools open and will pause on each breakpoint. - -* Make sure to [use the log](#log-from-your-script) so you can see what happens in your log output. Your script can log to the sitespeed.io default log. -~~~javascript -context.log.info('Info logging from your script'); -~~~ -* Run the script locally on your desktop without XVFB (using [npm version of sitespeed.io](https://www.npmjs.com/package/sitespeed.io)) so you can see in the browser window what happens. That is the easiest way to understand what's going on. -* If you use Docker and cannot run your test locally you can add --browsertime.videoParams.debug when you record the video. That way you will get one full video of all your scripts (but no Visual Metrics). -~~~bash -docker run --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:{% include version/sitespeed.io.txt %} https://www.sitespeed.io/ -n 1 --browsertime.videoParams.debug -~~~ -And then look at the video in the **data/video** folder. -* Use try/catch and await promises so you catch things that doesn't work. -~~~javascript -export default async function (context, commands) { - await commands.navigate('https://www.sitespeed.io'); - - await commands.measure.start(); - // Click on the link and wait on navigation to happen but try catch it so we can catch if it fails - try { - await commands.click.bySelectorAndWait('body > nav > div > div > div > ul > li:nth-child(2) > a'); - await commands.measure.stop(); - } catch(e) { - context.log.error('Could not click on ....'); - } -}; -~~~ -* If you use plain JavaScript you can copy/paste it and run it in your browsers console to make sure it really works. -* Take a [screenshot](/documentation/sitespeed.io/scripting/#screenshot) when your script fail to make it easier to see what's going on. -* If you navigate by clicking on elements you can verify that you end up where you want by running JavaScript. Here's an example where the new URL is logged but you can also verfify that it is the right one. - -~~~javascript -export default async function (context, commands) { - await commands.measure.start('https://www.sitespeed.io'); - // Hide everything - // We do not hide the body since the body needs to be visible when we do the magic to find the staret of the - // navigation by adding a layer of orange on top of the page - await commands.js.run('for (let node of document.body.childNodes) { if (node.style) node.style.display = "none";}'); - // Start measurning - await commands.measure.start(); - // Click on the link and wait on navigation to happen - await commands.click.bySelectorAndWait('body > nav > div > div > div > ul > li:nth-child(2) > a'); - await commands.measure.stop(); - - // Did we we really end up on the page that we wanted? Lets check! - const url = await commands.js.run('return window.location.href'); - context.log.info(`We ended up on ${url}`); -}; -~~~ -* If you run into trouble, please make sure you make it easy for us to [reproduce your problem](/documentation/sitespeed.io/bug-report/#explain-how-to-reproduce-your-issue) when you report a issue. - - -## Example code -Here are some examples on how you can use the scripting capabilities. - -### Measure the actual login step - -~~~javascript -export default async function (context, commands) { - // Navigate to a URL, but do not measure the URL - await commands.navigate( - 'https://en.wikipedia.org/w/index.php?title=Special:UserLogin&returnto=Main+Page' - ); - - try { - // Add text into an input field, finding the field by id - await commands.addText.byId('login', 'wpName1'); - await commands.addText.byId('password', 'wpPassword1'); - - // Start the measurement and give it the alias login - // The alias will be used when the metrics is sent to - // Graphite/InfluxDB - await commands.measure.start('login'); - - // Find the submit button and click it and wait for the - // page complete check to finish on the next loaded URL - await commands.click.byIdAndWait('wpLoginAttempt'); - // Stop and collect the metrics - return commands.measure.stop(); - } catch (e) { - // We try/catch so we will catch if the the input fields can't be found - // The error is automatically logged in Browsertime an rethrown here - // We could have an alternative flow ... - // else we can just let it cascade since it caught later on and reported in - // the HTML - throw e; - } -}; -~~~ - -### Measure the login step and more pages - -~~~javascript -export default async function (context, commands) { - // We start by navigating to the login page. - await commands.navigate( - 'https://en.wikipedia.org/w/index.php?title=Special:UserLogin&returnto=Main+Page' - ); - - // When we fill in a input field/click on a link we wanna - // try/catch that if the HTML on the page changes in the feature - // sitespeed.io will automatically log the error in a user friendly - // way, and the error will be re-thrown so you can act on it. - try { - // Add text into an input field, finding the field by id - await commands.addText.byId('login', 'wpName1'); - await commands.addText.byId('password', 'wpPassword1'); - - // Start the measurement before we click on the - // submit button. Sitespeed.io will start the video recording - // and prepare everything. - await commands.measure.start('login'); - // Find the sumbit button and click it and then wait - // for the pageCompleteCheck to finish - await commands.click.byIdAndWait('wpLoginAttempt'); - // Stop and collect the measurement before the next page we want to measure - await commands.measure.stop(); - // Measure the Barack Obama page as a logged in user - await commands.measure.start( - 'https://en.wikipedia.org/wiki/Barack_Obama' - ); - // And then measure the president page - return commands.measure.start('https://en.wikipedia.org/wiki/President_of_the_United_States'); - } catch (e) { - // We try/catch so we will catch if the the input fields can't be found - // The error is automatically logged in Browsertime and re-thrown here - // We could have an alternative flow ... - // else we can just let it cascade since it caught later on and reported in - // the HTML - throw e; - } -}; -~~~ - -### Measure one page after you logged in - -Testing a page after you have logged in: -First create a script that logs in the user (login.js): - -~~~javascript -export default async function (context, commands) { - await commands.navigate( - 'https://en.wikipedia.org/w/index.php?title=Special:UserLogin&returnto=Main+Page' - ); - - try { - await commands.addText.byId('login', 'wpName1'); - await commands.addText.byId('password', 'wpPassword1'); - // Click on the submit button with id wpLoginAttempt - await commands.click.byIdAndWait('wpLoginAttempt'); - // wait on a specific id to appear on the page after you logged in - return commands.wait.byId('pt-userpage', 10000); - } catch (e) { - // We try/catch so we will catch if the the input fields can't be found - // The error is automatically logged in Browsertime and re-thrown here - // We could have an alternative flow ... - // else we can just let it cascade since it caught later on and reported in - // the HTML - throw e; - } -}; -~~~ - -Then access the page that you want to test: - -~~~bash -sitespeed.io --preScript login.js https://en.wikipedia.org/wiki/Barack_Obama -~~~ - -#### More complicated login example - -~~~javascript -export default async function (context, commands) { - await commands.navigate( - 'https://example.org' - ); - try { - // Find the sign in button and click it - await commands.click.byId('sign_in_button'); - // Wait some time for the page to open a new login frame - await commands.wait.byTime(2000); - // Switch to the login frame - await commands.switch.toFrame('loginFrame'); - // Find the username fields by xpath (just as an example) - await commands.addText.byXpath( - 'peter@example.org', - '//*[@id="userName"]' - ); - // Click on the next button - await commands.click.byId('verifyUserButton'); - // Wait for the GUI to display the password field so we can select it - await commands.wait.byTime(2000); - // Wait for the actual password field - await commands.wait.byId('password', 5000); - // Fill in the password - await commands.addText.byId('dejh8Ghgs6ga(1217)', 'password'); - // Click the submit button - await commands.click.byId('btnSubmit'); - // In your implementation it is probably better to wait for an id - await commands.wait.byTime(5000); - // Measure the next page as a logged in user - return commands.measure.start( - 'https://example.org/logged/in/page' - ); - } catch(e) { - // We try/catch so we will catch if the the input fields can't be found - // We could have an alternative flow ... - // else we can just let it cascade since it caught later on and reported in - // the HTML - throw e; - } -}; -~~~ - -### Measure multiple pages - -Test multiple pages in a script: - -~~~javascript -export default async function (context, commands) { - await commands.measure.start('https://www.sitespeed.io'); - await commands.measure.start('https://www.sitespeed.io/examples/'); - return commands.measure.start('https://www.sitespeed.io/documentation/'); -}; -~~~ - -### Measure multiple pages and start white - -If you test multiple pages you will see that the layout is kept in the browser until the first paint of the new page. You can hack that by removing the current body and set the background color to white. Then every video will start white. - -~~~javascript -export default async function (context, commands) { - await commands.measure.start('https://www.sitespeed.io'); - await commands.js.run('document.body.innerHTML = ""; document.body.style.backgroundColor = "white";'); - await commands.measure.start('https://www.sitespeed.io/examples/'); - await commands.js.run('document.body.innerHTML = ""; document.body.style.backgroundColor = "white";'); - return commands.measure.start('https://www.sitespeed.io/documentation/'); -}; -~~~ - -### Scroll the page to measure Cumulative Layout Shift - -To get the Cumulative Layout Shift metric for Chrome closer to what real users get you can scroll the page and measure that. Depending on how your page work, you may want to tune the delay between the scrolling. - - -~~~javascript -export default async function (context, commands) { - const delayTime = 250; - - await commands.measure.start(); - await commands.navigate( - 'https://www.sitespeed.io/documentation/sitespeed.io/performance-dashboard/' - ); - await commands.scroll.toBottom(delayTime); - return commands.measure.stop(); -}; -~~~ - -### Add your own metrics -You can add your own metrics by adding the extra JavaScript that is executed after the page has loaded BUT did you know that also can add your own metrics directly through scripting? The metrics will be added to the metric tab in the HTML output and automatically sent to Graphite/InfluxDB. - -In this example we collect the temperature from our Android phone that runs the tests: - -~~~javascript -export default async function (context, commands) { - // Get the temperature from the phone - const temperature = await commands.android.shell("dumpsys battery | grep temperature | grep -Eo '[0-9]{1,3}'"); - // Start the test - await commands.measure.start( - 'https://www.sitespeed.io' - ); - // This is the magic where we add that new metric. It needs to happen - // after measure.start so we know where that metric belong - commands.measure.add('batteryTemperature', temperature/10); -}; -~~~ - -In this example we collect the number of comments on a blog post using commands.js.run() -to collect an element, use regex to parse out the number, and add it back as a custom metric. - -~~~javascript -export default async function (context, commands) { - await commands.measure.start('blog-post'); //alias is now blog-post - await commands.navigate('https://www.exampleBlog/blog-post'); - - //use commands.js.run to return the element using pure javascript - const element = await commands.js.run('return(document.getElementsByClassName("comment-count")[0].innerText)'); - - //parse out just the number of comments - var elementMetric = element.match(/\d/)[0]; - - // need to stop the measurement before you can add it as a metric - await commands.measure.stop(); - - // metric will now be added to the html and outpout to graphite/influx if you're using it - await commands.measure.add('commentsCount', elementMetric); -}; -~~~ - -### Measure shopping/checkout process -One of the really cool things with scripting is that you can measure all the pages in a checkout process. This is an example shop where you put one item in your cart and checkout as a guest. - -~~~javascript -export default async function (context, commands) { - // Start by measuring the first page of the shop - await commands.measure.start('https://shop.example.org'); - - // Then the product page - // Either your shop has a generic item used for testing that you can use - // or in real life you maybe need to add a check that the item really exists in stock - // and if not, try another product - await commands.measure.start('https://shop.example.org/prodcucs/theproduct'); - - // Add the item to your cart - await commands.click.bySelector('.add-to-cart'); - - // Go to the cart (and measure it) - await commands.measure.start('https://shop.example.org/cart/'); - - // Checkout as guest but you could also login as a customer - // We hide the HTML to avoid that the click on the link will - // fire First Visual Change. Best case you don't need to but we - // want an complex example - await commands.js.run('for (let node of document.body.childNodes) { if (node.style) node.style.display = "none";}'); - await commands.measure.start('CheckoutAsGuest'); - await commands.click.bySelectorAndWait('.checkout-as-guest'); - // Make sure to stop measuring and collect the metrics for the CheckoutAsGuest step - await commands.measure.stop(); - - // Finish your checkout - await commands.js.run('document.body.style.display = "none"'); - await commands.measure.start('FinishCheckout'); - await commands.click.bySelectorAndWait('.checkout-finish'); - // And collect metrics for the FinishCheckout step - return commands.measure.stop(); - // In a real web shop you probably can't finish the last step or you can return the item - // so the stock is correct. Either you do that at the end of your script or you - // add the item id in the context object like context.itemId = yyyy. Then in your - // postScript you can do what's needed with that id. -}; -~~~ - -### Log from your script - -You can log to the same output as sitespeed.io: - -~~~javascript -export default async function (context, commands) { - context.log.info('Info logging from your script'); - context.log.error('Error logging from your script'); -}; -~~~ - -### Pass your own options to your script -You can add your own parameters to the options object (by adding a parameter) and then pick them up in the script. The scripts runs in the context of browsertime, so you need to pass it in via that context. - -For example: you wanna pass on a password to your script, you can do that by adding --browsertime.my.password MY_PASSWORD and then in your code get a hold of that with: - -~~~javascript -export default async function (context, commands) { - // We are in browsertime context so you can skip that from your options object - context.log.info(context.options.my.password); -}; -~~~ - -If you use a configuration file you can pass on options like this: -~~~json -{ - "browsertime": { - "my": { - "password": "paAssW0rd" - } - } -} -~~~ - - - -### Error handling -You can try/catch failing commands that throw errors. If an error is not caught in your script, it will be caught in sitespeed.io and the error will be logged and reported in the HTML and to your data storage (Graphite/InfluxDb) under the key *browsertime.statistics.errors*. - -If you do catch the error, you should make sure you report it yourself with the [error command](#error), so you can see that in the HTML. This is needed for all errors except navigating/measuring a URL. They will automatically be reported (since they are always important). - -Here's an example of catching a URL that don't work and still continue to test another one. Remember since a navigation fails, this will be reported automatically and you don't need to do anything. - -~~~javascript -export default async function (context, commands) { - await commands.measure.start('https://www.sitespeed.io'); - try { - await commands.measure.start('https://nonworking.url/'); - } catch (e) {} - return commands.measure.start('https://www.sitespeed.io/documentation/'); -}; -~~~ - -You can also create your own errors. The error will be reported in the HTML and sent to Graphite/InfluxDB. If you report an error, the exit code from sitespeed.io will be > 0. - -~~~javascript -export default async function (context, commands) { - // ... - try { - // Click on a link - await commands.click.byLinkTextAndWait('Checkout'); - } catch (e) { - // Oh no, the content team has changed the name of the link! - commands.error('The link named Checkout do not exist on the page'); - // Since the error is reported, you can alert on it in Grafana - } -}; -~~~ - -### Measuring First Input Delay - FID -One of the new metrics Google is pushing is [First Input Delay](https://developers.google.com/web/updates/2018/05/first-input-delay). You can use it when you collect RUM but it can be hard to know what the user is doing. The recommended way is to use the Long Task API but the truth is that the attribution from the API is ... well can be better. When you have a long task, it is really hard to know why by looking at the attribution. - -How do we measure FID with sitespeed.io? You can measure clicks and button using the [Selenium Action API](https://selenium.dev/selenium/docs/api/javascript/module/selenium-webdriver/lib/input_exports_Actions.html) and then sitespeed.io uses the `first-input` performance observer to get it. What's really cool is that you can really measure it, instead of doing guestimates. - -Here's an example on measuring open the navigation on Wikipedia on mobile. I run my tests on a Alacatel One phone. - -~~~javascript -export default async function (context, commands) { - // We have some Selenium context - const webdriver = context.selenium.webdriver; - const driver = context.selenium.driver; - - // Start to measure - await commands.measure.start(); - // Go to a page ... - await commands.navigate('https://en.m.wikipedia.org/wiki/Barack_Obama'); - - // When the page has finished loading you can find the navigation and click on it - const actions = driver.actions(); - const nav = await driver.findElement( - webdriver.By.xpath('//*[@id="mw-mf-main-menu-button"]') - ); - await actions.click(nav).perform(); - - // Measure everything, that means you will run the JavaScript that collects the first input delay - return commands.measure.stop(); -}; -~~~ - -You will see the metric in the page summary and in the metrics section. - -![First input delay]({{site.baseurl}}/img/first-input-delay.png){:loading="lazy"} -{: .img-thumbnail} - -You can do mouse click, key press but there's no good way to do swiping as we know using the [Selenium Action API](https://selenium.dev/selenium/docs/api/javascript/module/selenium-webdriver/lib/input_exports_Actions.html). Your action will run after the page has loaded. If you wanna know what kind potential input delay you can have on load, you can use the *maxPotentialFid* metric that you will get by enabling `--cpu`. - -### Test multiple URLs - -If you want to test multiple URLs and need to do some specific things before each URL, you can do something like this (we pass on our [own options](#pass-your-own-options-to-your-script) to the script): - -~~~javascript -module.exports = async function (context, commands) { - const urls = context.options.urls; - for (let url of urls) { - // Do the stuff for each url that you need to do - // Maybe login a user or add a cookie or something - // Then test the URL - await commands.measure.start(url); - // When the test is finished, clear the browser cache - await commands.cache.clear(); - // Navigate to a blank page so you kind of start from scratch for the next URL - await commands.navigate('about:blank'); - } -}; -~~~ - -Then run your tests like this: - -~~~bash -docker run --rm -v "$(pwd):/sitespeed.io" sitespeedio/sitespeed.io:{% include version/sitespeed.io.txt %} testMultipleUrls.js --multi --browsertime.urls https://www.sitespeed.io --browsertime.urls https://www.sitespeed.io/documentation -n 1 -~~~ - -Or if you use JSON configuration, the same configuration looks like this: - -~~~json -{ - "browsertime": { - "urls": ["url1", "url2", "url3"] - } -} -~~~ - -## Tips and Tricks - -### Include the script in the HTML result -If you wanna keep of what script you are running, you can include the script into the HTML result with ```--html.showScript```. You will then get a link to a page that show the script. - -![Page to page]({{site.baseurl}}/img/script-link.png){:loading="lazy"} -{: .img-thumbnail} - -### Getting correct Visual Metrics -Visual metrics is the metrics that are collected using the video recording of the screen. In most cases that will work just out of the box. One thing to know is that when you go from one page to another page, the browser keeps the layout of the old page. That means that your video will start with the first page (instead of white) when you navigate to the next page. - -It will look like this: -![Page to page]({{site.baseurl}}/img/filmstrip-multiple-pages.jpg){:loading="lazy"} -{: .img-thumbnail} - -This is perfectly fine in most cases. But if you want to start white (the metrics somehow isn't correct) or if you click a link and that click changes the layout and is caught as First Visual Change, there are workarounds. - -If you just want to start white and navigate to the next page you can just clear the HTML between pages: - -~~~javascript -export default async function (context, commands) { - await commands.measure.start('https://www.sitespeed.io'); - // Renove the HTML and make sure the background is white - await commands.js.run('document.body.innerHTML = ""; document.body.style.backgroundColor = "white";'); - return commands.measure.start('https://www.sitespeed.io/examples/'); -}; -~~~ - -If you want to click a link and want to make sure that the HTML doesn't change when you click the link, you can try to hide the HTML and then click the link. - -~~~javascript -export default async function (context, commands) { - await commands.measure.start('https://www.sitespeed.io'); - // Hide everything - // We do not hide the body since the body needs to be visible when we do the magic to find the staret of the - // navigation by adding a layer of orange on top of the page - await commands.js.run('for (let node of document.body.childNodes) { if (node.style) node.style.display = "none";}'); - // Start measurning - await commands.measure.start(); - // Click on the link and wait on navigation to happen - await commands.click.bySelectorAndWait('body > nav > div > div > div > ul > li:nth-child(2) > a'); - return commands.measure.stop(); -}; -~~~ - -### Getting values from your page -In some scenarios you want to do different things dependent on what shows on your page. For example: You are testing a shop checkout and you need to verify that the item is in stock. You can run JavaScript and get the value back to your script. - -Here's an simple example, IRL you will need to get something from the page: - -~~~javascript -export default async function (context, commands) { - // We are in browsertime context so you can skip that from your options object - const secretValue = await commands.js.run('return 12'); - // if secretValue === 12 ... -} -~~~ - -If you want to have different flows depending on a element exists you can do something like this: - -~~~javascript -... -const exists = await commands.js.run('return (document.getElementById("nonExistsingID") != null) '); -if (exists) { - // The element with that id exists -} else { - // There's no element with that id -} -~~~ - -### Test one page that need a much longer page complete check than others - -If you have one page that needs some special handling that maybe do a couple of late and really slow AJAX requests, you can catch that with your on wait for the page to finish. - -~~~javascript -export default async function (context, commands) { - // First test a couple pages with default page complete check - await commands.measure.start('https://'); - await commands.measure.start('https://'); - await commands.measure.start('https://'); - - // Then we have a page that we know need to wait longer, start measuring - await command.measure.start('MySpecialPage'); - // Go to the page - await commands.navigate('https://'); - // Then you need to wait on a specific element or event. In this case - // we wait for a id to appear but you could also run your custom JS - await commands.wait.byId('my-id', 20000); - // And then when you know that page has loaded stop the measurement - // = stop the video, collect metrics etc - return commands.measure.stop(); -}; -~~~ - - -### Test the same page multiple times within the same run - -If you for some reason want to test the same URL within the same run multiple times, it will not work out of the box since the current version create the result files using the URL. For example testing https://www.sitespeed.io/ two times, will break since the second access will try to overwrite the first one. - -But there is a hack you can do. If you add a dummy query parameter (and give the page an alias) you can test them twice a - -~~~javascript -export default async function (context, commands) { - await commands.measure.start('https://www.sitespeed.io/', 'HomePage'); - - // Do something smart that then make you need to test the same URL again - // ... - - return commands.navigate('https://www.sitespeed.io/?dummy', 'BackToHomepage'); -}; -~~~ - -## Commands - -All commands will return a promise and you should await it to fulfil. If some command do not work, we will log that automatically and rethrow the error, so you can catch that and can act on that. - -The commands that ends with a **...AndWait** will wait for a new page to load, so use them only when you are clicking on a link and want a new page or view to load. - -### Measure -The measure command will prepare everything for measuring navigating to a new URL (clearing internal metrics, starting the video etc). If you give an URL to the measure command it will start to measure and navigate to that URL. - -If you do not give it a URL, it will prepare everything and start the video. So it's up to you to navigate/click on a link/submit the page. You also need to stop the measurement so that Browsertime/sitespeed.io knows that you want the metrics. - -#### measure.start(url) -Start and navigate to the URL and then automatically call the stop() function after the page has stopped navigating decided by the current pageCompleteCheck. - -~~~javascript -export default async function (context, commands) { - await commands.measure.start('https://www.sitespeed.io'); - // If you want to measure multiple URLs after each other - // you can just line them up - await commands.measure.start('https://www.sitespeed.io/examples/'); - return commands.measure.start('https://www.sitespeed.io/documentation/'); -}; -~~~ - -#### measure.start(url, alias) -Start and navigate to the URL and then automatically call the stop() function after the page has stopped navigating decided by the current pageCompleteCheck. The page will also get the alias that will be used when you send the metrics to Graphite/InfluxDB. Use it when you have complex URLs. - -~~~javascript -export default async function (context, commands) { - // Measure the page and give it the alias StartPage - return commands.measure.start('https://www.sitespeed.io', 'StartPage'); -}; -~~~ - -#### measure.start() -Start to measure. Browsertime/sitespeed.io will pick up the next URL and measure that. You need to call the stop() function yourself. - -~~~javascript -export default async function (context, commands) { - // Start by navigating to a page - await commands.navigate('https://www.example.org'); - // Start a measurement - await commands.measure.start(); - await commands.click.bySelectorAndWait('.important-link'); - // Remember that when you start() a measurement without a URL you also needs to stop it! - return commands.measure.stop(); -}; -~~~ - -If you start a measurement without giving a URL you need to also call measure.stop() when you finished measuring. -{: .note .note-warning} - -#### measure.start(alias) -Start to measure. Browsertime/sitespeed.io will pick up the next URL and measure that. You need to call the stop() function yourself. The page will also get the alias that will be used when you send the metrics to Graphite/InfluxDB. Use it when you have complex URLs. - -~~~javascript -export default async function (context, commands) { - // Start by navigating to a page - await commands.navigate('https://www.example.org'); - // Start a measurement and give it an alias that is used if you send the metrics to Graphite/InfluxDB for the next URL - await commands.measure.start('FancyName'); - await commands.click.bySelectorAndWait('.important-link'); - // Remember that when you start() a measurement without a URL you also needs to stop it! - return commands.measure.stop(); -}; -~~~ - -If you start a measurement without giving a URL you need to also call measure.stop() when you finished measuring. -{: .note .note-warning} - -#### measure.stop() -Stop measuring. This will collect technical metrics from the browser, stop the video recording, collect CPU data etc. - -#### measure.add(name, value) -Add your own measurements directly from your script. The data will be availible in the HTML on the metrics page and automatically sent to Graphite/InfluxDB. - -To be able to add any metrics, you need to have started a measurements. - -~~~javascript -export default async function (context, commands) { - // Get the temperature from the phone - const temperature = await commands.android.shell("dumpsys battery | grep temperature | grep -Eo '[0-9]{1,3}'"); - // Start the test - await commands.measure.start( - 'https://www.sitespeed.io' - ); - commands.measure.add('batteryTemperature', temperature/10); -}; -~~~ - -And you will get that metric in the HTML: - -![Adding metrics from your script]({{site.baseurl}}/img/batteryTemperatureMetric.png){:loading="lazy"} -{: .img-thumbnail} - - -#### measure.addObject(object) -You can also add multiple metrics in one go. -~~~javascript -export default async function (context, commands) { - - const extraMetrics = { a: 1, b: 2, c: 3}; - // Start the test - await commands.measure.start( - 'https://www.sitespeed.io' - ); - commands.measure.addObject(extraMetrics); -}; -~~~ - -And it will look like this: - -![Multiple metrics from a script]({{site.baseurl}}/img/scriptMetrics.png){:loading="lazy"} -{: .img-thumbnail} - - -And you can also add deep nested objects (no support in the HTML yet though, only in the data source). - -~~~javascript -export default async function (context, commands) { - - const extraMetrics = { android: {cpu: {temperature: 27, cores: 2}}}; - // Start the test - await commands.measure.start( - 'https://www.sitespeed.io' - ); - commands.measure.addObject(extraMetrics); -}; -~~~ - -### Stop Watch -If need to measure something that is not a navigation, you can do that by using a stop watch and measure the time. - - -#### stopWatch.get -You give your stop watch a name (that name will be used for the metric in the result). - -Get your stop watch like this: - -~~~javascript -const stopWatch = commands.stopWatch.get('My_watch'); -~~~ - -#### start() - -When you get the watch it's automatically started. You can restart the watch: - -~~~javascript - stopWatch.start(); -~~~ - -#### stop() or stopAndAdd() - -You stop your stop watch by either just stop it or stop it and add the metric to the last tested page. - -~~~javascript - // Stop the watch - stopWatch.stop(); - // Or stop the watch and add it to the page - stopWatch.stopAndAdd(); -~~~ - -If you want to measure how long time somethings takes before you navigate to a page you should follow this pattern: - -~~~javascript -export default async function (context, commands) { - const stopWatch = commands.stopWatch.get('Before_navigating_page'); - // Do the thing you want to measure ... - // Then stop the watch - const time = stopWatch.stop(); - // Measure navigation to a page - await commands.measure.start( - 'https://www.sitespeed.io' - ); - // Then attach that timing to that page. - commands.measure.add(stopWatch.getName(), time); -} -~~~ - -If you already measured a page and want to attach the metric to that page you can follow this pattern: - -~~~javascript -export default async function (context, commands) { - - await commands.measure.start( - 'https://www.sitespeed.io' - ); - const stopWatch = commands.stopWatch.get('After_navigating_page'); - // Do the thing you want to measure ... - stopWatch.stopAndAdd(); -} -~~~ - -### Breakpoint - -You can use breakpoints to debug your script. You can add breakpoints to your script that will be used when you run in `--debug` mode. At each breakpoint the browser will pause. You can continue by adding `window.browsertime.pause=false;` in your developer console. - -Debug mode works in Chrome/Firefox/Edge when running on desktop. It do not work in Docker and on mobile. When you run in debug mode, devtools will be automatically open so you can debug your script. - -In debug mode, the browser will pause after each iteration. - -~~~javascript -export default async function (context, commands) { - await commands.measure.start('https://www.sitespeed.io'); - await commands.breakpoint(''); - return commands.measure.start('https://www.sitespeed.io/documentation/'); -}; -~~~ - - -### Click -The click command will click on links. - -All click commands have two different versions: One that will return a promise when the link has been clicked and one that will return a promise that will be fullfilled when the link has been clicked and the browser navigated to the new URL and the [page complete check](/documentation/sitespeed.io/browsers/#choose-when-to-end-your-test) is done. - -If it does not find the link, it will throw an error, so make sure to catch it if you want an alternative flow. -{: .note .note-warning} - -#### click.byClassName(className) -Click on element that is found by specific class name. Will use ```document.getElementsByClassName(className)``` and take the first result and click on it. - -#### click.byClassNameAndWait(className) -Click on element that is found by specific class name and wait for [page load complete check](/documentation/sitespeed.io/browsers/#choose-when-to-end-your-test) to finish. Will use ```document.getElementsByClassName(className)``` and take the first result and click on it. - -#### click.byLinkText(text) -Click on link whose visible text matches the given string. Internally we use an xpath expression to find the correct link. - -#### click.byLinkTextAndWait(text) -Click on link whose visible text matches the given string and wait for [page complete check](/documentation/sitespeed.io/browsers/#choose-when-to-end-your-test) to finish. - -#### click.byPartialLinkText(text) -Click on link whose visible text contains the given substring. - -#### click.byPartialLinkTextAndWait(text) -Click on link whose visible text contains the given substring and wait for [page complete check](/documentation/sitespeed.io/browsers/#choose-when-to-end-your-test) to finish. - -#### click.byXpath(xpath) -Click on link that matches a XPath selector. - -#### click.byXpathAndWait(xpath) -Click on link that matches a XPath selector and wait for [page load complete check](/documentation/sitespeed.io/browsers/#choose-when-to-end-your-test) to finish. - -#### click.byJs(js) -Click on a link/element located by a JavaScript expression. Internally this will append a `.click()` to the JavaScript expression (for example if you add the JavaScript `document.querySelector("a")` to select the element, the backend code will run `document.querySelector("a").click()`). The result of this expression must be an element or list of elements. - -#### click.byJsAndWait(js) -Click on a link located by JavaScript expression. Internally this will append a `.click()` to the JavaScript expression. The result of this expression must be an element or list of elements. And wait for [page complete check](/documentation/sitespeed.io/browsers/#choose-when-to-end-your-test) to finish. - -#### click.byId(id) -Click on link located by the ID attribute. Internally we use ```document.getElementById(id)``` to get the correct element. - -#### click.byIdAndWait(id) -Click on link located by the ID attribute. Internally we use ```document.getElementById(id)``` to get the correct element. And wait for [page complete check](/documentation/sitespeed.io/browsers/#choose-when-to-end-your-test) to finish. - -#### click.bySelector(selector) -Click on element that is found by the CSS selector that has the given value. Internally we use ```document.querySelector(selector)``` to get the correct element. - -#### click.bySelectorAndWait(selector) -Click on element that is found by name CSS selector that has the given value and wait for the [page complete check](/documentation/sitespeed.io/browsers/#choose-when-to-end-your-test) to happen. Internally we use ```document.querySelector(selector)``` to get the correct element. - -#### click.byName(name) -Click on element located by the name. Internally we use ```document.querySelector``` to get the correct element. -### Mouse -The mouse command will perform various mouse events. - -#### mouse.moveTo.byXpath(xpath) -Move mouse to an element that matches a XPath selector. - -#### mouse.moveTo.bySelector(selector) -Move mouse to an element that matches a CSS selector. - -#### mouse.moveTo.toPosition(xPos, yPos) -Move mouse to a given position. - -#### mouse.moveTo.byOffset(xOff, yOff) -Move mouse by a given offset to current location. - -#### mouse.contextClick.byXpath(xpath) -Perform ContextClick on an element that matches a XPath selector. - -#### mouse.contextClick.bySelector(selector) -Perform ContextClick on an element that matches a CSS selector. -#### mouse.contextClick.atCursor() -Perform ContextClick at the cursor's position. - -#### mouse.singleClick.byXpath(xpath, options) -Perform mouse single click on an element matches a XPath selector. Options is an optional parameter, and if the property 'wait' is set to true, browsertime will wait until the pageCompleteCheck has finished. - -#### mouse.singleClick.bySelector(selector, options) -Perform mouse single click on an element matches a CSS selector. Options is an optional parameter, and if the property 'wait' is set to true, browsertime will wait until the pageCompleteCheck has finished. -#### mouse.singleClick.atCursor(options) -Perform mouse single click at the cursor's position. Options is an optional parameter, and if the property 'wait' is set to true, browsertime will wait until the pageCompleteCheck has finished. - -#### mouse.doubleClick.byXpath(xpath, options) -Perform double single click on an element matches a XPath selector. Options is an optional parameter, and if the property 'wait' is set to true, browsertime will wait until the pageCompleteCheck has finished. - -#### mouse.doubleClick.bySelector(selector, options) -Perform double single click on an element matches a CSS selector. Options is an optional parameter, and if the property 'wait' is set to true, browsertime will wait until the pageCompleteCheck has finished. - -#### mouse.doubleClick.atCursor(options) -Perform mouse double click at the cursor's position. Options is an optional parameter, and if the property 'wait' is set to true, browsertime will wait until the pageCompleteCheck has finished. - -#### mouse.clickAndHold.byXpath(xpath) -Click and hold an element that matches a XPath selector. - -#### mouse.clickAndHold.bySelector(selector) -Click and hold an element that matches a CSS selector. - -#### mouse.clickAndHold.atCursor() -Click and hold an element at the cursor's position. - -#### mouse.clickAndHold.atPosition(xPos, yPos) -Click and hold an element at the specified position. - -#### mouse.clickAndHold.releaseAtXpath(xpah) -Release mouse on element that matches the specified Xpath. - -#### mouse.clickAndHold.releaseAtSelector(selector) -Release mouse on element that matches the specified CSS selector. - -#### mouse.clickAndHold.releaseAtPosition(xPos, yPos) -Release mouse at specified coordinates. - -### Wait -There are a couple of help commands that makes it easier to wait. Either you can wait on a specific id to appear or for x amount of milliseconds. - -#### wait.byTime(ms) -Wait for x ms. - -#### wait.byId(id,maxTime) -Wait for an element with id to appear before maxTime. The element needs to be visible for the user. If the element do not appear within maxTime an error will be thrown. - -#### wait.byXpath(xpath, maxTime) -Wait for an element found by xpath to appear before maxTime. The element needs to be visible for the user. If the element do not appear within maxTime an error will be thrown. - -#### wait.bySelector(selector, maxTime) -Wait for an element found by selector to appear before maxTime. The element needs to be visible for the user. If the element do not appear within maxTime an error will be thrown. - -#### wait.byCondition(condition, maxTime) -Wait for a JavaScript condition that eventually will be a truthy-value before maxTime. If the condition do not met within maxTime an error will be thrown. - -You pass on your JavaScript condition like: `wait.byCondition("document.querySelector('a.active').innerHTML === 'Start'");` - -#### wait.byPageToComplete() -Wait for the page to finish loading by using the configured [page complete check](/documentation/sitespeed.io/browsers/#choose-when-to-end-your-test). This can be useful if you use Selenium to click on elements and want to wait on a new page to load. - -### Run JavaScript -You can run your own JavaScript in the browser from your script. - -#### js.run(javascript) -Run JavaScript. Will throw an error if the JavaScript fails. - -If you want to get values from the web page, this is your best friend. Make sure to return the value and you can use it in your script. - -~~~javascript -export default async function (context, commands) { - // We are in browsertime context so you can skip that from your options object - const secretValue = await commands.js.run('return 12'); - // if secretValue === 12 ... -} -~~~ - -By default this will return a [Selenium WebElement](https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_WebElement.html). - -#### js.runAndWait(javascript) -Run JavaScript and wait for [page complete check](/documentation/sitespeed.io/browsers/#choose-when-to-end-your-test). This is perfect if you wanna click on links with pure JavaScript and measure a URL. Will throw an error if the JavaScript fails. - -### Navigate -Navigate/go to a URL without measuring it. - -#### navigate(url) -Navigate to a URL and do not measure it. It will use the default [page complete check](/documentation/sitespeed.io/browsers/#choose-when-to-end-your-test) and follow the exact same pattern for going to a page as normal Browsertime navigation except it will skip collecting any metrics. - -#### navigation.back() -Navigate backward in history. - -#### navigation.forward() -Navigate forward in history. - -#### navigation.refresh() -Refresh page. - -### Scroll -Scroll the page. - -#### scroll.byPixels(xPixels, yPixels) -Scroll the page by the specified pixels. -#### scroll.byPages(pages) -Scroll the page by the specified pages. - -#### scroll.toBottom(delayTime) -Scroll to the bottom of the page. Will scroll by pages and wait the delay time between each scroll. Default delay time is 250 ms. - -~~~javascript -export default async function (context, commands) { - // ... navigate to page ... - await commands.scroll.toBottom(); -} -~~~ -#### scroll.byLines(lines) -Scroll the page by the specified lines. Only supported by Firefox. - -### Add text -You can add text to input elements. The element needs to visible. You can also send pressable keys as Unicode PUA([PrivateUser Area](https://en.wikipedia.org/wiki/Private_Use_Areas)) format. - -#### addText.byId(text, id) -Add the *text* to the element with the *id*. If the id is not found the command will throw an error. - -#### addText.byXpath(text, xpath) -Add the *text* to the element by using *xpath*. If the xpath is not found the command will throw an error. - -#### addText.bySelector(text, selector) -Add the *text* to the element by using *CSS selector*. If the xpath is not found the command will throw an error. - -#### addText.byName(text, name) -Add the *text* to the element by using the attribute name. If the element is not found the command will throw an error. - -#### addText.byClassName(text, className) -Add the *text* to the element by using class name. If the element is not found the command will throw an error. - -### Screenshot -Take a screenshot. The image is stored in the screenshot directory for the URL you are testing. This can be super helpful to use in a catch block if something fails. If you use sitespeed.io you can find the image in the screenshot tab for each individual run. - -![Screenshots]({{site.baseurl}}/img/multiple-screenshots.jpg){:loading="lazy"} -{: .img-thumbnail-center} - -#### screenshot.take(name) -Give your screenshot a name and it will be used together with the iteration index to store the image. - -### Switch -You can switch to iframes or windows if that is needed. - -If frame/window is not found, an error will be thrown. -{: .note .note-warning} - -#### switch.toFrame(id) -Switch to a frame by its id. - - -#### switch.toFrameByXpath(xpath) -Switch to window by xpath. -#### switch.toFrameBySelector(selector) -Switch to window by CSS selector. -#### switch.toWindow(name) -Switch to window by name. - -#### switch.toParentFrame -Switch to the parent frame. - -#### switch.toNewTab(url) -Create a new tab and switch to it. Url parameter is optional which will trigger a navigation ot the given url. - -#### switch.toNewWindow(url) -Create a new window and switch to it. Url parameter is optional which will trigger a navigation ot the given url. - -### Set - -Raw set value of elements. - -#### set.innerHtml(html, selector) -Use a CSS selector to find the element and set the html to innerHtml. Internally it uses ```document.querySelector(selector)``` to find the right element. - -#### set.innerHtmlById(html, id) - -Use the id to find the element and set the html to innerHtml. Internally it uses ```document.getElementById(id)``` to find the right element. - -#### set.innerText(text, selector) -Use a CSS selector to find the element and set the text to innerText. Internally it uses ```document.querySelector(selector)``` to find the right element. - -#### set.innerTextById(text, id) -Use the id to find the element and set the text to innerText. Internally it uses ```document.getElementById(id)``` to find the right element. - -#### set.value(value, selector) -Use a CSS selector to find the element and set the value to value. Internally it uses ```document.querySelector(selector)``` to find the right element. - -#### set.valueById(value, id) -Use the id to find the element and set the value to value. Internally it uses ```document.getElementById(id)``` to find the right element. - -### Cache -There's an experimental command for clearing the cache. The command works both for Chrome and Firefox. Use it when you want to clear the browser cache between different URLs. - -#### cache.clear() -Clear the browser cache. Remove cache and cookies. - -~~~javascript -export default async function (context, commands) { - // First you probably visit a couple of pages and then clear the cache - await commands.cache.clear(); - // And then visit another page -} -~~~ - -#### cache.clearKeepCookies() -Clear the browser cache but keep cookies. - -~~~javascript -export default async function (context, commands) { - // If you have login cookies that lives really long you may want to test aceesing the page as a logged in user - // but without a browser cache. You can try that with ... - - // Login the user and the clear the cache but keep cookies - await commands.cache.clearKeepCookies(); - // and then access the URL you wanna test. -} -~~~ - -### Chrome DevTools Protocol -Send messages to Chrome using the [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/). This only works in Chrome/Edge at the moment. You can send, send and get and listen on events. - -#### cdp.send(command, args) -Send a command to Chrome and don't expect something back. - -Here's an example of injecting JavaScript that runs on every new document. - -~~~javascript -export default async function (context, commands) { - await commands.cdp.send('Page.addScriptToEvaluateOnNewDocument',{source: 'console.log("hello");'}); - await commands.measure.start('https://www.sitespeed.io'); -} -~~~ - -#### cdp.sendAndGet(command, args) -Send a command to Chrome and get the result back. - -~~~javascript -export default async function (context, commands) { - await commands.measure.start('https://www.sitespeed.io'); - const domCounters = await commands.cdp.sendAndGet('Memory.getDOMCounters'); - context.log.info('Memory.getDOMCounters %j', domCounters); - } -~~~ - -#### cdp.on(event, functionOnEvent) -You can listen to CDP events. Here's an example to get hold of all responses for a page. - -~~~javascript -export default async function (context, commands) { - const responses = []; - await commands.cdp.on('Network.responseReceived', params => { - responses.push(params); - }); - await commands.measure.start('https://www.sitespeed.io/search/'); - context.log.info('Responses %j', responses); -}; -~~~ - -#### cdp.dp.getRawClient() -Get the raw CDP client so you can do whatever you want. Here's an example on how to change the server header on the response. - -~~~javascript -export default async function (context, commands) { - const cdpClient = commands.cdp.getRawClient(); - await cdpClient.Fetch.enable({ - patterns: [ - { - urlPattern: '*', - requestStage: 'Response' - } - ] - }); - - cdpClient.Fetch.requestPaused(async reqEvent => { - const { requestId } = reqEvent; - let responseHeaders = reqEvent.responseHeaders || []; - - const newServerHeader = { name: 'server', value: 'Haxxor' }; - const foundHeaderIndex = responseHeaders.findIndex( - h => h.name === 'server' - ); - if (foundHeaderIndex) { - responseHeaders[foundHeaderIndex] = newServerHeader; - } else { - responseHeaders.push(newServerHeader); - } - - return cdpClient.Fetch.continueResponse({ - requestId, - responseCode: 200, - responseHeaders - }); - }); - - await commands.measure.start('https://www.sitespeed.io/search/'); -} -~~~ -### Error -You can create your own error. The error will be attached to the latest tested page. Say that you have a script where you first measure a page and then want to click on a specific link and the link doesn't exist. Then you can attach your own error with your own error text. The error will be sent to your datasource and will be visible in the HTML result. - -~~~javascript -export default async function (context, commands) { - // Start by navigating to a page - await commands.navigate('https://www.example.org'); - // Start a measurement - await commands.measure.start(); - try { - await commands.click.bySelectorAndWait('.important-link'); - } catch(e) { - // Ooops we couldn't click the link - commands.error('.important-link does not exist on the page'); - } - // Remember that when you start() a measurement without a URL you also needs to stop it! - return commands.measure.stop(); -}; -~~~ - -#### error(message) -Create an error. Use it if you catch a thrown error, want to continue with something else, but still report the error. - - -### Select -Select command for selecting an option in a drop-down field. - -#### select.selectByIdAndValue(selectId, value) -Select a field by the id of the select element and the value of the option. - -#### select.selectByNameAndValue(selectName, value) -Select a field by the name of the select element and the value of the option. -#### select.selectByIdAndIndex(selectId, index) -Select a field by the id of the select element and the index of the option. -#### select.selectByNameAndIndex(selectName, index) -Select a field by the name of the select element and the index of the option. - -#### select.deselectById(selectId) -Deselect a field by the id of the select element. -#### select.getValuesById(selectId) -Get the values of all options in a select field by the id of the select element. -#### select.getSelectedValueById(selectId) -Get the value of the selected option in a select field by the id of the select element. -### Meta data -Add meta data to your script. The extra data will be visible in the HTML result page. - -Setting meta data like this: - -~~~javascript -export default async function (context, commands) { - commands.meta.setTitle('Test Grafana SPA'); - commands.meta.setDescription('Test the first page, click the timepicker and then choose Last 30 days and measure that page.'); - await commands.measure.start( - 'https://dashboard.sitespeed.io/d/000000044/page-timing-metrics?orgId=1','pageTimingMetricsDefault' - ); - await commands.click.byClassName('gf-timepicker-nav-btn'); - await commands.wait.byTime(1000); - await commands.measure.start('pageTimingMetrics30Days'); - await commands.click.byLinkTextAndWait('Last 30 days'); - await commands.measure.stop(); -}; -~~~ - -Will result in: - -![Title and description for a script]({{site.baseurl}}/img/titleanddesc.png){:loading="lazy"} -{: .img-thumbnail} - -#### meta.setTitle(title) -Add a title of your script. The title is text only. - -#### meta.setDescription(desc) -Add a description of your script. The description can be text/HTML. - -### Android -If you run your tests in an Android phone you probably want to interact with your phone throught the shell. - -~~~javascript -export default async function (context, commands) { - // Get the temperature from the phone - const temperature = await commands.android.shell("dumpsys battery | grep temperature | grep -Eo '[0-9]{1,3}'"); - context.log.info('The battery temperature is %s', temperature/10); - // Start the test - return commands.measure.start( - 'https://www.sitespeed.io' - ); -}; -~~~ - -#### android.shell(command) -Run a shell command directly on your phone. - -### Use Selenium directly -You can use Selenium directly if you need to use things that are not available through our commands. We use the NodeJS flavor of Selenium. - -You get a hold of the Selenium objects through the context. - -The *selenium.webdriver* is the Selenium [WebDriver public API object](https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/index.html). And *selenium.driver* is the [instantiated version of the WebDriver](https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_WebDriver.html) driving the current version of the browser. - -Checkout this example to see how you can use them. - -~~~javascript -export default async function (context, commands) { - // We fetch the selenium webdriver from context - // The selenium-webdriver - // https://www.selenium.dev/selenium/docs/api/javascript/module/selenium-webdriver/index.html - const seleniumWebdriver = context.selenium.webdriver; - // The driver exposes for example By that you use to find elements - const By = seleniumWebdriver.By; - - // We use the driver to find an element - const seleniumDriver = context.selenium.driver; - - // To navigate to a new page it is best to use our navigation commands - // so the script waits until the page is loaded - await commands.navigate('https://www.sitespeed.io'); - - // Lets use Selenium to find the Documentation link - const seleniumElement = await seleniumDriver.findElement(By.linkText('Documentation')); - - // So now we actually got a Selenium WebElement - // https://www.selenium.dev/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_WebElement.html - context.log.info('The element tag is ', await seleniumElement.getTagName()); - - // We then use our command to start a measurement - await commands.measure.start('DocumentationPage'); - - // Use the Selebium WebElement and click on it - await seleniumElement.click(); - // We make sure to wait for the new page to load - await commands.wait.byPageToComplete(); - - // Stop the measuerment - return commands.measure.stop(); -} -~~~ - -If you need help with Selenium, checkout [the official Selenium documentation](https://www.seleniumhq.org/docs/). - diff --git a/docs/documentation/sitespeed.io/scripting/scripts/core.js b/docs/documentation/sitespeed.io/scripting/scripts/core.js new file mode 100644 index 0000000000..6344a3d9fb --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/scripts/core.js @@ -0,0 +1,702 @@ +/* global document */ +var accordionLocalStorageKey = 'accordion-id'; +var themeLocalStorageKey = 'theme'; +var fontSizeLocalStorageKey = 'font-size'; +var html = document.querySelector('html'); + +var MAX_FONT_SIZE = 30; +var MIN_FONT_SIZE = 10; + +// eslint-disable-next-line no-undef +var localStorage = window.localStorage; + +function getTheme() { + var theme = localStorage.getItem(themeLocalStorageKey); + + if (theme) return theme; + + theme = document.body.getAttribute('data-theme'); + + switch (theme) { + case 'dark': + case 'light': + return theme; + case 'fallback-dark': + if ( + // eslint-disable-next-line no-undef + window.matchMedia('(prefers-color-scheme)').matches && + // eslint-disable-next-line no-undef + window.matchMedia('(prefers-color-scheme: light)').matches + ) { + return 'light'; + } + + return 'dark'; + + case 'fallback-light': + if ( + // eslint-disable-next-line no-undef + window.matchMedia('(prefers-color-scheme)').matches && + // eslint-disable-next-line no-undef + window.matchMedia('(prefers-color-scheme: dark)').matches + ) { + return 'dark'; + } + + return 'light'; + + default: + return 'dark'; + } +} + +function localUpdateTheme(theme) { + var body = document.body; + var svgUse = document.querySelectorAll('.theme-svg-use'); + var iconID = theme === 'dark' ? '#light-theme-icon' : '#dark-theme-icon'; + + body.setAttribute('data-theme', theme); + body.classList.remove('dark', 'light'); + body.classList.add(theme); + + svgUse.forEach(function (svg) { + svg.setAttribute('xlink:href', iconID); + }); +} + +function updateTheme(theme) { + localUpdateTheme(theme); + localStorage.setItem(themeLocalStorageKey, theme); +} + +function toggleTheme() { + var body = document.body; + var theme = body.getAttribute('data-theme'); + + var newTheme = theme === 'dark' ? 'light' : 'dark'; + + updateTheme(newTheme); +} + +(function () { + var theme = getTheme(); + + updateTheme(theme); +})(); + +/** + * Function to set accordion id to localStorage. + * @param {string} id Accordion id + */ +function setAccordionIdToLocalStorage(id) { + /** + * @type {object} + */ + var ids = JSON.parse(localStorage.getItem(accordionLocalStorageKey)); + + ids[id] = id; + localStorage.setItem(accordionLocalStorageKey, JSON.stringify(ids)); +} + +/** + * Function to remove accordion id from localStorage. + * @param {string} id Accordion id + */ +function removeAccordionIdFromLocalStorage(id) { + /** + * @type {object} + */ + var ids = JSON.parse(localStorage.getItem(accordionLocalStorageKey)); + + delete ids[id]; + localStorage.setItem(accordionLocalStorageKey, JSON.stringify(ids)); +} + +/** + * Function to get all accordion ids from localStorage. + * + * @returns {object} + */ +function getAccordionIdsFromLocalStorage() { + /** + * @type {object} + */ + var ids = JSON.parse(localStorage.getItem(accordionLocalStorageKey)); + + return ids || {}; +} + +function toggleAccordion(element) { + var currentNode = element; + var isCollapsed = currentNode.getAttribute('data-isopen') === 'false'; + + if (isCollapsed) { + currentNode.setAttribute('data-isopen', 'true'); + setAccordionIdToLocalStorage(currentNode.id); + } else { + currentNode.setAttribute('data-isopen', 'false'); + removeAccordionIdFromLocalStorage(currentNode.id); + } +} + +function initAccordion() { + if ( + localStorage.getItem(accordionLocalStorageKey) === undefined || + localStorage.getItem(accordionLocalStorageKey) === null + ) { + localStorage.setItem(accordionLocalStorageKey, '{}'); + } + var allAccordion = document.querySelectorAll('.sidebar-section-title'); + var ids = getAccordionIdsFromLocalStorage(); + + allAccordion.forEach(function (item) { + item.addEventListener('click', function () { + toggleAccordion(item); + }); + if (item.id in ids) { + toggleAccordion(item); + } + }); +} + +function isSourcePage() { + return Boolean(document.querySelector('#source-page')); +} + +function bringElementIntoView(element, updateHistory = true) { + // If element is null then we are not going further + if (!element) { + return; + } + + /** + * tocbotInstance is defined in layout.tmpl + * It is defined when we are initializing tocbot. + * + */ + // eslint-disable-next-line no-undef + if (tocbotInstance) { + setTimeout( + // eslint-disable-next-line no-undef + () => tocbotInstance.updateTocListActiveElement(element), + 60 + ); + } + var navbar = document.querySelector('.navbar-container'); + var body = document.querySelector('.main-content'); + var elementTop = element.getBoundingClientRect().top; + + var offset = 16; + + if (navbar) { + offset += navbar.scrollHeight; + } + + if (body) { + body.scrollBy(0, elementTop - offset); + } + + if (updateHistory) { + // eslint-disable-next-line no-undef + history.pushState(null, null, '#' + element.id); + } +} + +// eslint-disable-next-line no-unused-vars +function bringLinkToView(event) { + event.preventDefault(); + event.stopPropagation(); + var id = event.currentTarget.getAttribute('href'); + + if (!id) { + return; + } + + var element = document.getElementById(id.slice(1)); + + if (element) { + bringElementIntoView(element); + } +} + +function bringIdToViewOnMount() { + if (isSourcePage()) { + return; + } + + // eslint-disable-next-line no-undef + var id = window.location.hash; + + if (id === '') { + return; + } + + var element = document.getElementById(id.slice(1)); + + if (!element) { + id = decodeURI(id); + element = document.getElementById(id.slice(1)); + } + + if (element) { + bringElementIntoView(element, false); + } +} + +function createAnchorElement(id) { + var anchor = document.createElement('a'); + + anchor.textContent = '#'; + anchor.href = '#' + id; + anchor.classList.add('link-anchor'); + anchor.onclick = bringLinkToView; + + return anchor; +} + +function addAnchor() { + var main = document.querySelector('.main-content').querySelector('section'); + + var h1 = main.querySelectorAll('h1'); + var h2 = main.querySelectorAll('h2'); + var h3 = main.querySelectorAll('h3'); + var h4 = main.querySelectorAll('h4'); + + var targets = [h1, h2, h3, h4]; + + targets.forEach(function (target) { + target.forEach(function (heading) { + var anchor = createAnchorElement(heading.id); + + heading.classList.add('has-anchor'); + heading.append(anchor); + }); + }); +} + +/** + * + * @param {string} value + */ +function copy(value) { + const el = document.createElement('textarea'); + + el.value = value; + document.body.appendChild(el); + el.select(); + document.execCommand('copy'); + document.body.removeChild(el); +} + +function showTooltip(id) { + var tooltip = document.getElementById(id); + + tooltip.classList.add('show-tooltip'); + setTimeout(function () { + tooltip.classList.remove('show-tooltip'); + }, 3000); +} + +/* eslint-disable-next-line */ +function copyFunction(id) { + // selecting the pre element + var code = document.getElementById(id); + + // selecting the ol.linenums + var element = code.querySelector('.linenums'); + + if (!element) { + // selecting the code block + element = code.querySelector('code'); + } + + // copy + copy(element.innerText.trim().replace(/(^\t)/gm, '')); + + // show tooltip + showTooltip('tooltip-' + id); +} + +function hideTocOnSourcePage() { + if (isSourcePage()) { + document.querySelector('.toc-container').style.display = 'none'; + } +} + +function getPreTopBar(id, lang = '') { + // tooltip + var tooltip = '
Copied!
'; + + // template of copy to clipboard icon container + var copyToClipboard = + ''; + + var langNameDiv = + '
' + + lang.toLocaleUpperCase() + + '
'; + + var topBar = + '
' + + langNameDiv + + copyToClipboard + + '
'; + + return topBar; +} + +function getPreDiv() { + var divElement = document.createElement('div'); + + divElement.classList.add('pre-div'); + + return divElement; +} + +function processAllPre() { + var targets = document.querySelectorAll('pre'); + var footer = document.querySelector('#PeOAagUepe'); + var navbar = document.querySelector('#VuAckcnZhf'); + + var navbarHeight = 0; + var footerHeight = 0; + + if (footer) { + footerHeight = footer.getBoundingClientRect().height; + } + + if (navbar) { + navbarHeight = navbar.getBoundingClientRect().height; + } + + // eslint-disable-next-line no-undef + var preMaxHeight = window.innerHeight - navbarHeight - footerHeight - 250; + + targets.forEach(function (pre, idx) { + var parent = pre.parentNode; + + if (parent && parent.getAttribute('data-skip-pre-process') === 'true') { + return; + } + + var div = getPreDiv(); + var id = 'ScDloZOMdL' + idx; + + var lang = pre.getAttribute('data-lang') || 'code'; + var topBar = getPreTopBar(id, lang); + + div.innerHTML = topBar; + + pre.style.maxHeight = preMaxHeight + 'px'; + pre.id = id; + pre.classList.add('prettyprint'); + pre.parentNode.insertBefore(div, pre); + div.appendChild(pre); + }); +} + +function highlightAndBringLineIntoView() { + // eslint-disable-next-line no-undef + var lineNumber = window.location.hash.replace('#line', ''); + + try { + var selector = '[data-line-number="' + lineNumber + '"'; + + var element = document.querySelector(selector); + + element.scrollIntoView(); + element.parentNode.classList.add('selected'); + } catch (error) { + console.error(error); + } +} + +function getFontSize() { + var currentFontSize = 16; + + try { + currentFontSize = Number.parseInt( + html.style.fontSize.split('px')[0], + 10 + ); + } catch (error) { + console.log(error); + } + + return currentFontSize; +} + +function localUpdateFontSize(fontSize) { + html.style.fontSize = fontSize + 'px'; + + var fontSizeText = document.querySelector( + '#b77a68a492f343baabea06fad81f651e' + ); + + if (fontSizeText) { + fontSizeText.innerHTML = fontSize; + } +} + +function updateFontSize(fontSize) { + localUpdateFontSize(fontSize); + localStorage.setItem(fontSizeLocalStorageKey, fontSize); +} + +(function () { + var fontSize = getFontSize(); + var fontSizeInLocalStorage = localStorage.getItem(fontSizeLocalStorageKey); + + if (fontSizeInLocalStorage) { + var n = Number.parseInt(fontSizeInLocalStorage, 10); + + if (n === fontSize) { + return; + } + updateFontSize(n); + } else { + updateFontSize(fontSize); + } +})(); + +// eslint-disable-next-line no-unused-vars +function incrementFont(event) { + var n = getFontSize(); + + if (n < MAX_FONT_SIZE) { + updateFontSize(n + 1); + } +} + +// eslint-disable-next-line no-unused-vars +function decrementFont(event) { + var n = getFontSize(); + + if (n > MIN_FONT_SIZE) { + updateFontSize(n - 1); + } +} + +function fontSizeTooltip() { + var fontSize = getFontSize(); + + return ` +
+ +
+ ${fontSize} +
+ + +
+ + `; +} + +function initTooltip() { + // add tooltip to navbar item + // eslint-disable-next-line no-undef + tippy('.theme-toggle', { + content: 'Toggle Theme', + delay: 500, + }); + + // eslint-disable-next-line no-undef + tippy('.search-button', { + content: 'Search', + delay: 500, + }); + + // eslint-disable-next-line no-undef + tippy('.font-size', { + content: 'Change font size', + delay: 500, + }); + + // eslint-disable-next-line no-undef + tippy('.codepen-button', { + content: 'Open code in CodePen', + placement: 'left', + }); + + // eslint-disable-next-line no-undef + tippy('.copy-code', { + content: 'Copy this code', + placement: 'left', + }); + + // eslint-disable-next-line no-undef + tippy('.font-size', { + content: fontSizeTooltip(), + trigger: 'click', + interactive: true, + allowHTML: true, + placement: 'left', + }); +} + +function fixTable() { + const tables = document.querySelectorAll('table'); + + for (const table of tables) { + if (table.classList.contains('hljs-ln')) { + // don't want to wrap code blocks. + return; + } + + var div = document.createElement('div'); + + div.classList.add('table-div'); + table.parentNode.insertBefore(div, table); + div.appendChild(table); + } +} + +function hideMobileMenu() { + var mobileMenuContainer = document.querySelector('#mobile-sidebar'); + var target = document.querySelector('#mobile-menu'); + var svgUse = target.querySelector('use'); + + if (mobileMenuContainer) { + mobileMenuContainer.classList.remove('show'); + } + if (target) { + target.setAttribute('data-isopen', 'false'); + } + if (svgUse) { + svgUse.setAttribute('xlink:href', '#menu-icon'); + } +} + +function showMobileMenu() { + var mobileMenuContainer = document.querySelector('#mobile-sidebar'); + var target = document.querySelector('#mobile-menu'); + var svgUse = target.querySelector('use'); + + if (mobileMenuContainer) { + mobileMenuContainer.classList.add('show'); + } + if (target) { + target.setAttribute('data-isopen', 'true'); + } + if (svgUse) { + svgUse.setAttribute('xlink:href', '#close-icon'); + } +} + +function onMobileMenuClick() { + var target = document.querySelector('#mobile-menu'); + var isOpen = target.getAttribute('data-isopen') === 'true'; + + if (isOpen) { + hideMobileMenu(); + } else { + showMobileMenu(); + } +} + +function initMobileMenu() { + var menu = document.querySelector('#mobile-menu'); + + if (menu) { + menu.addEventListener('click', onMobileMenuClick); + } +} + +function addHrefToSidebarTitle() { + var titles = document.querySelectorAll('.sidebar-title-anchor'); + + titles.forEach(function (title) { + // eslint-disable-next-line no-undef + title.setAttribute('href', baseURL); + }); +} + +function onDomContentLoaded() { + var themeButton = document.querySelectorAll('.theme-toggle'); + + initMobileMenu(); + + if (themeButton) { + themeButton.forEach(function (button) { + button.addEventListener('click', toggleTheme); + }); + } + + // Highlighting code + + // eslint-disable-next-line no-undef + hljs.addPlugin({ + 'after:highlightElement': function (obj) { + // Replace 'code' with result.language when + // we are able to cross-check the correctness of + // result. + obj.el.parentNode.setAttribute('data-lang', 'code'); + }, + }); + // eslint-disable-next-line no-undef + hljs.highlightAll(); + // eslint-disable-next-line no-undef + hljs.initLineNumbersOnLoad({ + singleLine: true, + }); + + // Highlight complete + + initAccordion(); + addAnchor(); + processAllPre(); + hideTocOnSourcePage(); + setTimeout(function () { + bringIdToViewOnMount(); + if (isSourcePage()) { + highlightAndBringLineIntoView(); + } + }, 1000); + initTooltip(); + fixTable(); + addHrefToSidebarTitle(); +} + +// eslint-disable-next-line no-undef +window.addEventListener('DOMContentLoaded', onDomContentLoaded); + +// eslint-disable-next-line no-undef +window.addEventListener('hashchange', (event) => { + const url = new URL(event.newURL); + + if (url.hash !== '') { + bringIdToViewOnMount(url.hash); + } +}); + +// eslint-disable-next-line no-undef +window.addEventListener('storage', (event) => { + if (event.newValue === 'undefined') return; + + initTooltip(); + + if (event.key === themeLocalStorageKey) localUpdateTheme(event.newValue); + if (event.key === fontSizeLocalStorageKey) + localUpdateFontSize(event.newValue); +}); diff --git a/docs/documentation/sitespeed.io/scripting/scripts/core.min.js b/docs/documentation/sitespeed.io/scripting/scripts/core.min.js new file mode 100644 index 0000000000..6165f9ffa9 --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/scripts/core.min.js @@ -0,0 +1,23 @@ +var accordionLocalStorageKey="accordion-id",themeLocalStorageKey="theme",fontSizeLocalStorageKey="font-size",html=document.querySelector("html"),MAX_FONT_SIZE=30,MIN_FONT_SIZE=10,localStorage=window.localStorage;function getTheme(){var e=localStorage.getItem(themeLocalStorageKey);if(e)return e;switch(e=document.body.getAttribute("data-theme")){case"dark":case"light":return e;case"fallback-dark":return window.matchMedia("(prefers-color-scheme)").matches&&window.matchMedia("(prefers-color-scheme: light)").matches?"light":"dark";case"fallback-light":return window.matchMedia("(prefers-color-scheme)").matches&&window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light";default:return"dark"}}function localUpdateTheme(e){var t=document.body,o=document.querySelectorAll(".theme-svg-use"),n="dark"===e?"#light-theme-icon":"#dark-theme-icon";t.setAttribute("data-theme",e),t.classList.remove("dark","light"),t.classList.add(e),o.forEach(function(e){e.setAttribute("xlink:href",n)})}function updateTheme(e){localUpdateTheme(e),localStorage.setItem(themeLocalStorageKey,e)}function toggleTheme(){updateTheme("dark"===document.body.getAttribute("data-theme")?"light":"dark")}function setAccordionIdToLocalStorage(e){var t=JSON.parse(localStorage.getItem(accordionLocalStorageKey));t[e]=e,localStorage.setItem(accordionLocalStorageKey,JSON.stringify(t))}function removeAccordionIdFromLocalStorage(e){var t=JSON.parse(localStorage.getItem(accordionLocalStorageKey));delete t[e],localStorage.setItem(accordionLocalStorageKey,JSON.stringify(t))}function getAccordionIdsFromLocalStorage(){return JSON.parse(localStorage.getItem(accordionLocalStorageKey))||{}}function toggleAccordion(e){"false"===e.getAttribute("data-isopen")?(e.setAttribute("data-isopen","true"),setAccordionIdToLocalStorage(e.id)):(e.setAttribute("data-isopen","false"),removeAccordionIdFromLocalStorage(e.id))}function initAccordion(){void 0!==localStorage.getItem(accordionLocalStorageKey)&&null!==localStorage.getItem(accordionLocalStorageKey)||localStorage.setItem(accordionLocalStorageKey,"{}");var e=document.querySelectorAll(".sidebar-section-title"),t=getAccordionIdsFromLocalStorage();e.forEach(function(e){e.addEventListener("click",function(){toggleAccordion(e)}),e.id in t&&toggleAccordion(e)})}function isSourcePage(){return Boolean(document.querySelector("#source-page"))}function bringElementIntoView(e,t=!0){var o,n,i,c;e&&(tocbotInstance&&setTimeout(()=>tocbotInstance.updateTocListActiveElement(e),60),o=document.querySelector(".navbar-container"),n=document.querySelector(".main-content"),i=e.getBoundingClientRect().top,c=16,o&&(c+=o.scrollHeight),n&&n.scrollBy(0,i-c),t&&history.pushState(null,null,"#"+e.id))}function bringLinkToView(e){e.preventDefault(),e.stopPropagation();var e=e.currentTarget.getAttribute("href");!e||(e=document.getElementById(e.slice(1)))&&bringElementIntoView(e)}function bringIdToViewOnMount(){var e,t;isSourcePage()||""!==(e=window.location.hash)&&((t=document.getElementById(e.slice(1)))||(e=decodeURI(e),t=document.getElementById(e.slice(1))),t&&bringElementIntoView(t,!1))}function createAnchorElement(e){var t=document.createElement("a");return t.textContent="#",t.href="#"+e,t.classList.add("link-anchor"),t.onclick=bringLinkToView,t}function addAnchor(){var e=document.querySelector(".main-content").querySelector("section");[e.querySelectorAll("h1"),e.querySelectorAll("h2"),e.querySelectorAll("h3"),e.querySelectorAll("h4")].forEach(function(e){e.forEach(function(e){var t=createAnchorElement(e.id);e.classList.add("has-anchor"),e.append(t)})})}function copy(e){const t=document.createElement("textarea");t.value=e,document.body.appendChild(t),t.select(),document.execCommand("copy"),document.body.removeChild(t)}function showTooltip(e){var t=document.getElementById(e);t.classList.add("show-tooltip"),setTimeout(function(){t.classList.remove("show-tooltip")},3e3)}function copyFunction(e){var t=document.getElementById(e);copy((t.querySelector(".linenums")||t.querySelector("code")).innerText.trim().replace(/(^\t)/gm,"")),showTooltip("tooltip-"+e)}function hideTocOnSourcePage(){isSourcePage()&&(document.querySelector(".toc-container").style.display="none")}function getPreTopBar(e,t=""){e='";return'
'+('
'+t.toLocaleUpperCase()+"
")+e+"
"}function getPreDiv(){var e=document.createElement("div");return e.classList.add("pre-div"),e}function processAllPre(){var e=document.querySelectorAll("pre"),t=document.querySelector("#PeOAagUepe"),o=document.querySelector("#VuAckcnZhf"),n=0,i=0,c=(t&&(i=t.getBoundingClientRect().height),o&&(n=o.getBoundingClientRect().height),window.innerHeight-n-i-250);e.forEach(function(e,t){var o,n=e.parentNode;n&&"true"===n.getAttribute("data-skip-pre-process")||(n=getPreDiv(),o=getPreTopBar(t="ScDloZOMdL"+t,e.getAttribute("data-lang")||"code"),n.innerHTML=o,e.style.maxHeight=c+"px",e.id=t,e.classList.add("prettyprint"),e.parentNode.insertBefore(n,e),n.appendChild(e))})}function highlightAndBringLineIntoView(){var e=window.location.hash.replace("#line","");try{var t='[data-line-number="'+e+'"',o=document.querySelector(t);o.scrollIntoView(),o.parentNode.classList.add("selected")}catch(e){console.error(e)}}function getFontSize(){var e=16;try{e=Number.parseInt(html.style.fontSize.split("px")[0],10)}catch(e){console.log(e)}return e}function localUpdateFontSize(e){html.style.fontSize=e+"px";var t=document.querySelector("#b77a68a492f343baabea06fad81f651e");t&&(t.innerHTML=e)}function updateFontSize(e){localUpdateFontSize(e),localStorage.setItem(fontSizeLocalStorageKey,e)}function incrementFont(e){var t=getFontSize();t + +
+ ${e} +
+ + + + + `}function initTooltip(){tippy(".theme-toggle",{content:"Toggle Theme",delay:500}),tippy(".search-button",{content:"Search",delay:500}),tippy(".font-size",{content:"Change font size",delay:500}),tippy(".codepen-button",{content:"Open code in CodePen",placement:"left"}),tippy(".copy-code",{content:"Copy this code",placement:"left"}),tippy(".font-size",{content:fontSizeTooltip(),trigger:"click",interactive:!0,allowHTML:!0,placement:"left"})}function fixTable(){for(const t of document.querySelectorAll("table")){if(t.classList.contains("hljs-ln"))return;var e=document.createElement("div");e.classList.add("table-div"),t.parentNode.insertBefore(e,t),e.appendChild(t)}}function hideMobileMenu(){var e=document.querySelector("#mobile-sidebar"),t=document.querySelector("#mobile-menu"),o=t.querySelector("use");e&&e.classList.remove("show"),t&&t.setAttribute("data-isopen","false"),o&&o.setAttribute("xlink:href","#menu-icon")}function showMobileMenu(){var e=document.querySelector("#mobile-sidebar"),t=document.querySelector("#mobile-menu"),o=t.querySelector("use");e&&e.classList.add("show"),t&&t.setAttribute("data-isopen","true"),o&&o.setAttribute("xlink:href","#close-icon")}function onMobileMenuClick(){("true"===document.querySelector("#mobile-menu").getAttribute("data-isopen")?hideMobileMenu:showMobileMenu)()}function initMobileMenu(){var e=document.querySelector("#mobile-menu");e&&e.addEventListener("click",onMobileMenuClick)}function addHrefToSidebarTitle(){document.querySelectorAll(".sidebar-title-anchor").forEach(function(e){e.setAttribute("href",baseURL)})}function onDomContentLoaded(){var e=document.querySelectorAll(".theme-toggle");initMobileMenu(),e&&e.forEach(function(e){e.addEventListener("click",toggleTheme)}),hljs.addPlugin({"after:highlightElement":function(e){e.el.parentNode.setAttribute("data-lang","code")}}),hljs.highlightAll(),hljs.initLineNumbersOnLoad({singleLine:!0}),initAccordion(),addAnchor(),processAllPre(),hideTocOnSourcePage(),setTimeout(function(){bringIdToViewOnMount(),isSourcePage()&&highlightAndBringLineIntoView()},1e3),initTooltip(),fixTable(),addHrefToSidebarTitle()}updateTheme(getTheme()),function(){var e=getFontSize(),t=localStorage.getItem(fontSizeLocalStorageKey);t?(t=Number.parseInt(t,10))!==e&&updateFontSize(t):updateFontSize(e)}(),window.addEventListener("DOMContentLoaded",onDomContentLoaded),window.addEventListener("hashchange",e=>{e=new URL(e.newURL);""!==e.hash&&bringIdToViewOnMount(e.hash)}),window.addEventListener("storage",e=>{"undefined"!==e.newValue&&(initTooltip(),e.key===themeLocalStorageKey&&localUpdateTheme(e.newValue),e.key===fontSizeLocalStorageKey&&localUpdateFontSize(e.newValue))}); \ No newline at end of file diff --git a/docs/documentation/sitespeed.io/scripting/scripts/resize.js b/docs/documentation/sitespeed.io/scripting/scripts/resize.js new file mode 100644 index 0000000000..d8e3a9cf7b --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/scripts/resize.js @@ -0,0 +1,90 @@ +/* global document */ +// This file is @deprecated + +var NAVBAR_OPTIONS = {}; + +(function() { + var NAVBAR_RESIZE_LOCAL_STORAGE_KEY = 'NAVBAR_RESIZE_LOCAL_STORAGE_KEY'; + + var navbar = document.querySelector('#navbar'); + var footer = document.querySelector('#footer'); + var mainSection = document.querySelector('#main'); + var localStorageResizeObject = JSON.parse( + // eslint-disable-next-line no-undef + localStorage.getItem(NAVBAR_RESIZE_LOCAL_STORAGE_KEY) + ); + + /** + * Check whether we have any resize value in local storage or not. + * If we have resize value then resize the navbar. + **/ + if (localStorageResizeObject) { + navbar.style.width = localStorageResizeObject.width; + mainSection.style.marginLeft = localStorageResizeObject.width; + footer.style.marginLeft = localStorageResizeObject.width; + } + + var navbarSlider = document.querySelector('#navbar-resize'); + + function resizeNavbar(event) { + var pageX = event.pageX, + pageXPlusPx = event.pageX + 'px', + min = Number.parseInt(NAVBAR_OPTIONS.min, 10) || 300, + max = Number.parseInt(NAVBAR_OPTIONS.max, 10) || 600; + + /** + * Just to add some checks. If min is smaller than 10 then + * user may accidentally end up reducing the size of navbar + * less than 10. In that case user will not able to resize navbar + * because navbar slider will be hidden. + */ + if (min < 10) { + min = 10; + } + + /** + * Only resize if pageX in range between min and max + * allowed value. + */ + if (min < pageX && pageX < max) { + navbar.style.width = pageXPlusPx; + mainSection.style.marginLeft = pageXPlusPx; + footer.style.marginLeft = pageXPlusPx; + } + } + + function setupEventListeners() { + // eslint-disable-next-line no-undef + window.addEventListener('mousemove', resizeNavbar); + // eslint-disable-next-line no-undef + window.addEventListener('touchmove', resizeNavbar); + } + + function afterRemovingEventListeners() { + // eslint-disable-next-line no-undef + localStorage.setItem( + NAVBAR_RESIZE_LOCAL_STORAGE_KEY, + JSON.stringify({ + width: navbar.style.width + }) + ); + } + + function removeEventListeners() { + // eslint-disable-next-line no-undef + window.removeEventListener('mousemove', resizeNavbar); + // eslint-disable-next-line no-undef + window.removeEventListener('touchend', resizeNavbar); + afterRemovingEventListeners(); + } + + navbarSlider.addEventListener('mousedown', setupEventListeners); + navbarSlider.addEventListener('touchstart', setupEventListeners); + // eslint-disable-next-line no-undef + window.addEventListener('mouseup', removeEventListeners); +})(); + +// eslint-disable-next-line no-unused-vars +function setupResizeOptions(options) { + NAVBAR_OPTIONS = options; +} diff --git a/docs/documentation/sitespeed.io/scripting/scripts/search.js b/docs/documentation/sitespeed.io/scripting/scripts/search.js new file mode 100644 index 0000000000..415e1cf8d1 --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/scripts/search.js @@ -0,0 +1,265 @@ +/* global document */ + +const searchId = 'LiBfqbJVcV'; +const searchHash = '#' + searchId; +const searchContainer = document.querySelector('#PkfLWpAbet'); +const searchWrapper = document.querySelector('#iCxFxjkHbP'); +const searchCloseButton = document.querySelector('#VjLlGakifb'); +const searchInput = document.querySelector('#vpcKVYIppa'); +const resultBox = document.querySelector('#fWwVHRuDuN'); + +function showResultText(text) { + resultBox.innerHTML = `${text}`; +} + +function hideSearch() { + // eslint-disable-next-line no-undef + if (window.location.hash === searchHash) { + // eslint-disable-next-line no-undef + history.go(-1); + } + + // eslint-disable-next-line no-undef + window.onhashchange = null; + + if (searchContainer) { + searchContainer.style.display = 'none'; + } +} + +function listenCloseKey(event) { + if (event.key === 'Escape') { + hideSearch(); + // eslint-disable-next-line no-undef + window.removeEventListener('keyup', listenCloseKey); + } +} + +function showSearch() { + try { + // Closing mobile menu before opening + // search box. + // It is defined in core.js + // eslint-disable-next-line no-undef + hideMobileMenu(); + } catch (error) { + console.error(error); + } + + // eslint-disable-next-line no-undef + window.onhashchange = hideSearch; + + // eslint-disable-next-line no-undef + if (window.location.hash !== searchHash) { + // eslint-disable-next-line no-undef + history.pushState(null, null, searchHash); + } + + if (searchContainer) { + searchContainer.style.display = 'flex'; + // eslint-disable-next-line no-undef + window.addEventListener('keyup', listenCloseKey); + } + + if (searchInput) { + searchInput.focus(); + } +} + +async function fetchAllData() { + // eslint-disable-next-line no-undef + const { hostname, protocol, port } = location; + + // eslint-disable-next-line no-undef + const base = protocol + '//' + hostname + (port !== '' ? ':' + port : '') + baseURL; + // eslint-disable-next-line no-undef + const url = new URL('data/search.json', base); + const result = await fetch(url); + const { list } = await result.json(); + + return list; +} + +// eslint-disable-next-line no-unused-vars +function onClickSearchItem(event) { + const target = event.currentTarget; + + if (target) { + const href = target.getAttribute('href') || ''; + let elementId = href.split('#')[1] || ''; + let element = document.getElementById(elementId); + + if (!element) { + elementId = decodeURI(elementId); + element = document.getElementById(elementId); + } + + if (element) { + setTimeout(function() { + // eslint-disable-next-line no-undef + bringElementIntoView(element); // defined in core.js + }, 100); + } + } +} + +function buildSearchResult(result) { + let output = ''; + const removeHTMLTagsRegExp = /(<([^>]+)>)/ig; + + for (const res of result) { + const { title = '', description = '' } = res.item; + + const _link = res.item.link.replace('.*/, ''); + const _title = title.replace(removeHTMLTagsRegExp, ""); + const _description = description.replace(removeHTMLTagsRegExp, ""); + + output += ` + +
${_title}
+
${_description || 'No description available.'}
+
+ `; + } + + return output; +} + +function getSearchResult(list, keys, searchKey) { + const defaultOptions = { + shouldSort: true, + threshold: 0.4, + location: 0, + distance: 100, + maxPatternLength: 32, + minMatchCharLength: 1, + keys: keys + }; + + const options = { ...defaultOptions }; + + // eslint-disable-next-line no-undef + const searchIndex = Fuse.createIndex(options.keys, list); + + // eslint-disable-next-line no-undef + const fuse = new Fuse(list, options, searchIndex); + + const result = fuse.search(searchKey); + + if (result.length > 20) { + return result.slice(0, 20); + } + + return result; +} + +function debounce(func, wait, immediate) { + let timeout; + + return function() { + const args = arguments; + + clearTimeout(timeout); + timeout = setTimeout(() => { + timeout = null; + if (!immediate) { + // eslint-disable-next-line consistent-this, no-invalid-this + func.apply(this, args); + } + }, wait); + + if (immediate && !timeout) { + // eslint-disable-next-line consistent-this, no-invalid-this + func.apply(this, args); + } + }; +} + +let searchData; + +async function search(event) { + const value = event.target.value; + const keys = ['title', 'description']; + + if (!resultBox) { + console.error('Search result container not found'); + + return; + } + + if (!value) { + showResultText('Type anything to view search result'); + + return; + } + + if (!searchData) { + showResultText('Loading...'); + + try { + // eslint-disable-next-line require-atomic-updates + searchData = await fetchAllData(); + } catch (e) { + console.log(e); + showResultText('Failed to load result.'); + + return; + } + } + + const result = getSearchResult(searchData, keys, value); + + if (!result.length) { + showResultText('No result found! Try some different combination.'); + + return; + } + + // eslint-disable-next-line require-atomic-updates + resultBox.innerHTML = buildSearchResult(result); +} + +function onDomContentLoaded() { + const searchButton = document.querySelectorAll('.search-button'); + const debouncedSearch = debounce(search, 300); + + if (searchCloseButton) { + searchCloseButton.addEventListener('click', hideSearch); + } + + if (searchButton) { + searchButton.forEach(function(item) { + item.addEventListener('click', showSearch); + }); + } + + if (searchContainer) { + searchContainer.addEventListener('click', hideSearch); + } + + if (searchWrapper) { + searchWrapper.addEventListener('click', function(event) { + event.stopPropagation(); + }); + } + + if (searchInput) { + searchInput.addEventListener('keyup', debouncedSearch); + } + + // eslint-disable-next-line no-undef + if (window.location.hash === searchHash) { + showSearch(); + } +} + +// eslint-disable-next-line no-undef +window.addEventListener('DOMContentLoaded', onDomContentLoaded); + +// eslint-disable-next-line no-undef +window.addEventListener('hashchange', function() { + // eslint-disable-next-line no-undef + if (window.location.hash === searchHash) { + showSearch(); + } +}); diff --git a/docs/documentation/sitespeed.io/scripting/scripts/search.min.js b/docs/documentation/sitespeed.io/scripting/scripts/search.min.js new file mode 100644 index 0000000000..5358bced85 --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/scripts/search.min.js @@ -0,0 +1,6 @@ +const searchId="LiBfqbJVcV",searchHash="#"+searchId,searchContainer=document.querySelector("#PkfLWpAbet"),searchWrapper=document.querySelector("#iCxFxjkHbP"),searchCloseButton=document.querySelector("#VjLlGakifb"),searchInput=document.querySelector("#vpcKVYIppa"),resultBox=document.querySelector("#fWwVHRuDuN");function showResultText(e){resultBox.innerHTML=`${e}`}function hideSearch(){window.location.hash===searchHash&&history.go(-1),window.onhashchange=null,searchContainer&&(searchContainer.style.display="none")}function listenCloseKey(e){"Escape"===e.key&&(hideSearch(),window.removeEventListener("keyup",listenCloseKey))}function showSearch(){try{hideMobileMenu()}catch(e){console.error(e)}window.onhashchange=hideSearch,window.location.hash!==searchHash&&history.pushState(null,null,searchHash),searchContainer&&(searchContainer.style.display="flex",window.addEventListener("keyup",listenCloseKey)),searchInput&&searchInput.focus()}async function fetchAllData(){var{hostname:e,protocol:t,port:n}=location,t=t+"//"+e+(""!==n?":"+n:"")+baseURL,e=new URL("data/search.json",t);const a=await fetch(e);n=(await a.json()).list;return n}function onClickSearchItem(t){const n=t.currentTarget;if(n){const a=n.getAttribute("href")||"";t=a.split("#")[1]||"";let e=document.getElementById(t);e||(t=decodeURI(t),e=document.getElementById(t)),e&&setTimeout(function(){bringElementIntoView(e)},100)}}function buildSearchResult(e){let t="";var n=/(<([^>]+)>)/gi;for(const s of e){const{title:c="",description:i=""}=s.item;var a=s.item.link.replace('.*/,""),o=c.replace(n,""),r=i.replace(n,"");t+=` + +
${o}
+
${r||"No description available."}
+
+ `}return t}function getSearchResult(e,t,n){var t={...{shouldSort:!0,threshold:.4,location:0,distance:100,maxPatternLength:32,minMatchCharLength:1,keys:t}},a=Fuse.createIndex(t.keys,e);const o=new Fuse(e,t,a),r=o.search(n);return 20{o=null,a||t.apply(this,e)},n),a&&!o&&t.apply(this,e)}}let searchData;async function search(e){e=e.target.value;if(resultBox)if(e){if(!searchData){showResultText("Loading...");try{searchData=await fetchAllData()}catch(e){return console.log(e),void showResultText("Failed to load result.")}}e=getSearchResult(searchData,["title","description"],e);e.length?resultBox.innerHTML=buildSearchResult(e):showResultText("No result found! Try some different combination.")}else showResultText("Type anything to view search result");else console.error("Search result container not found")}function onDomContentLoaded(){const e=document.querySelectorAll(".search-button");var t=debounce(search,300);searchCloseButton&&searchCloseButton.addEventListener("click",hideSearch),e&&e.forEach(function(e){e.addEventListener("click",showSearch)}),searchContainer&&searchContainer.addEventListener("click",hideSearch),searchWrapper&&searchWrapper.addEventListener("click",function(e){e.stopPropagation()}),searchInput&&searchInput.addEventListener("keyup",t),window.location.hash===searchHash&&showSearch()}window.addEventListener("DOMContentLoaded",onDomContentLoaded),window.addEventListener("hashchange",function(){window.location.hash===searchHash&&showSearch()}); \ No newline at end of file diff --git a/docs/documentation/sitespeed.io/scripting/scripts/third-party/Apache-License-2.0.txt b/docs/documentation/sitespeed.io/scripting/scripts/third-party/Apache-License-2.0.txt new file mode 100644 index 0000000000..75b52484ea --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/scripts/third-party/Apache-License-2.0.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/docs/documentation/sitespeed.io/scripting/scripts/third-party/fuse.js b/docs/documentation/sitespeed.io/scripting/scripts/third-party/fuse.js new file mode 100644 index 0000000000..a55c5daa07 --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/scripts/third-party/fuse.js @@ -0,0 +1,9 @@ +/** + * Fuse.js v6.4.6 - Lightweight fuzzy-search (http://fusejs.io) + * + * Copyright (c) 2021 Kiro Risk (http://kiro.me) + * All Rights Reserved. Apache Software License 2.0 + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ +var e,t;e=this,t=function(){"use strict";function e(t){return(e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(t)}function t(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function n(e,t){for(var n=0;ne.length)&&(t=e.length);for(var n=0,r=new Array(t);n0&&void 0!==arguments[0]?arguments[0]:3,t=new Map,n=Math.pow(10,e);return{get:function(e){var r=e.match(I).length;if(t.has(r))return t.get(r);var i=1/Math.sqrt(r),o=parseFloat(Math.round(i*n)/n);return t.set(r,o),o},clear:function(){t.clear()}}}var E=function(){function e(){var n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},r=n.getFn,i=void 0===r?A.getFn:r;t(this,e),this.norm=C(3),this.getFn=i,this.isCreated=!1,this.setIndexRecords()}return r(e,[{key:"setSources",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];this.docs=e}},{key:"setIndexRecords",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];this.records=e}},{key:"setKeys",value:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];this.keys=t,this._keysMap={},t.forEach((function(t,n){e._keysMap[t.id]=n}))}},{key:"create",value:function(){var e=this;!this.isCreated&&this.docs.length&&(this.isCreated=!0,g(this.docs[0])?this.docs.forEach((function(t,n){e._addString(t,n)})):this.docs.forEach((function(t,n){e._addObject(t,n)})),this.norm.clear())}},{key:"add",value:function(e){var t=this.size();g(e)?this._addString(e,t):this._addObject(e,t)}},{key:"removeAt",value:function(e){this.records.splice(e,1);for(var t=e,n=this.size();t2&&void 0!==arguments[2]?arguments[2]:{},r=n.getFn,i=void 0===r?A.getFn:r,o=new E({getFn:i});return o.setKeys(e.map(_)),o.setSources(t),o.create(),o}function R(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=t.errors,r=void 0===n?0:n,i=t.currentLocation,o=void 0===i?0:i,c=t.expectedLocation,a=void 0===c?0:c,s=t.distance,u=void 0===s?A.distance:s,h=t.ignoreLocation,f=void 0===h?A.ignoreLocation:h,l=r/e.length;if(f)return l;var d=Math.abs(a-o);return u?l+d/u:d?1:l}function F(){for(var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[],t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:A.minMatchCharLength,n=[],r=-1,i=-1,o=0,c=e.length;o=t&&n.push([r,i]),r=-1)}return e[o-1]&&o-r>=t&&n.push([r,o-1]),n}function P(e){for(var t={},n=0,r=e.length;n1&&void 0!==arguments[1]?arguments[1]:{},o=i.location,c=void 0===o?A.location:o,a=i.threshold,s=void 0===a?A.threshold:a,u=i.distance,h=void 0===u?A.distance:u,f=i.includeMatches,l=void 0===f?A.includeMatches:f,d=i.findAllMatches,v=void 0===d?A.findAllMatches:d,g=i.minMatchCharLength,y=void 0===g?A.minMatchCharLength:g,p=i.isCaseSensitive,m=void 0===p?A.isCaseSensitive:p,k=i.ignoreLocation,M=void 0===k?A.ignoreLocation:k;if(t(this,e),this.options={location:c,threshold:s,distance:h,includeMatches:l,findAllMatches:v,minMatchCharLength:y,isCaseSensitive:m,ignoreLocation:M},this.pattern=m?n:n.toLowerCase(),this.chunks=[],this.pattern.length){var b=function(e,t){r.chunks.push({pattern:e,alphabet:P(e),startIndex:t})},x=this.pattern.length;if(x>32){for(var L=0,S=x%32,w=x-S;L3&&void 0!==arguments[3]?arguments[3]:{},i=r.location,o=void 0===i?A.location:i,c=r.distance,a=void 0===c?A.distance:c,s=r.threshold,u=void 0===s?A.threshold:s,h=r.findAllMatches,f=void 0===h?A.findAllMatches:h,l=r.minMatchCharLength,d=void 0===l?A.minMatchCharLength:l,v=r.includeMatches,g=void 0===v?A.includeMatches:v,y=r.ignoreLocation,p=void 0===y?A.ignoreLocation:y;if(t.length>32)throw new Error(L(32));for(var m,k=t.length,M=e.length,b=Math.max(0,Math.min(o,M)),x=u,S=b,w=d>1||g,_=w?Array(M):[];(m=e.indexOf(t,S))>-1;){var O=R(t,{currentLocation:m,expectedLocation:b,distance:a,ignoreLocation:p});if(x=Math.min(O,x),S=m+k,w)for(var j=0;j=K;J-=1){var T=J-1,U=n[e.charAt(T)];if(w&&(_[T]=+!!U),W[J]=(W[J+1]<<1|1)&U,P&&(W[J]|=(I[J+1]|I[J])<<1|1|I[J+1]),W[J]&$&&(C=R(t,{errors:P,currentLocation:T,expectedLocation:b,distance:a,ignoreLocation:p}))<=x){if(x=C,(S=T)<=b)break;K=Math.max(1,2*b-S)}}var V=R(t,{errors:P+1,currentLocation:b,expectedLocation:b,distance:a,ignoreLocation:p});if(V>x)break;I=W}var B={isMatch:S>=0,score:Math.max(.001,C)};if(w){var G=F(_,d);G.length?g&&(B.indices=G):B.isMatch=!1}return B}(e,n,i,{location:c+o,distance:a,threshold:s,findAllMatches:u,minMatchCharLength:h,includeMatches:r,ignoreLocation:f}),p=y.isMatch,m=y.score,k=y.indices;p&&(g=!0),v+=m,p&&k&&(d=[].concat(l(d),l(k)))}));var y={isMatch:g,score:g?v/this.chunks.length:1};return g&&r&&(y.indices=d),y}}]),e}(),D=function(){function e(n){t(this,e),this.pattern=n}return r(e,[{key:"search",value:function(){}}],[{key:"isMultiMatch",value:function(e){return z(e,this.multiRegex)}},{key:"isSingleMatch",value:function(e){return z(e,this.singleRegex)}}]),e}();function z(e,t){var n=e.match(t);return n?n[1]:null}var K=function(e){a(i,e);var n=f(i);function i(e){return t(this,i),n.call(this,e)}return r(i,[{key:"search",value:function(e){var t=e===this.pattern;return{isMatch:t,score:t?0:1,indices:[0,this.pattern.length-1]}}}],[{key:"type",get:function(){return"exact"}},{key:"multiRegex",get:function(){return/^="(.*)"$/}},{key:"singleRegex",get:function(){return/^=(.*)$/}}]),i}(D),q=function(e){a(i,e);var n=f(i);function i(e){return t(this,i),n.call(this,e)}return r(i,[{key:"search",value:function(e){var t=-1===e.indexOf(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,e.length-1]}}}],[{key:"type",get:function(){return"inverse-exact"}},{key:"multiRegex",get:function(){return/^!"(.*)"$/}},{key:"singleRegex",get:function(){return/^!(.*)$/}}]),i}(D),W=function(e){a(i,e);var n=f(i);function i(e){return t(this,i),n.call(this,e)}return r(i,[{key:"search",value:function(e){var t=e.startsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,this.pattern.length-1]}}}],[{key:"type",get:function(){return"prefix-exact"}},{key:"multiRegex",get:function(){return/^\^"(.*)"$/}},{key:"singleRegex",get:function(){return/^\^(.*)$/}}]),i}(D),J=function(e){a(i,e);var n=f(i);function i(e){return t(this,i),n.call(this,e)}return r(i,[{key:"search",value:function(e){var t=!e.startsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,e.length-1]}}}],[{key:"type",get:function(){return"inverse-prefix-exact"}},{key:"multiRegex",get:function(){return/^!\^"(.*)"$/}},{key:"singleRegex",get:function(){return/^!\^(.*)$/}}]),i}(D),T=function(e){a(i,e);var n=f(i);function i(e){return t(this,i),n.call(this,e)}return r(i,[{key:"search",value:function(e){var t=e.endsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[e.length-this.pattern.length,e.length-1]}}}],[{key:"type",get:function(){return"suffix-exact"}},{key:"multiRegex",get:function(){return/^"(.*)"\$$/}},{key:"singleRegex",get:function(){return/^(.*)\$$/}}]),i}(D),U=function(e){a(i,e);var n=f(i);function i(e){return t(this,i),n.call(this,e)}return r(i,[{key:"search",value:function(e){var t=!e.endsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,e.length-1]}}}],[{key:"type",get:function(){return"inverse-suffix-exact"}},{key:"multiRegex",get:function(){return/^!"(.*)"\$$/}},{key:"singleRegex",get:function(){return/^!(.*)\$$/}}]),i}(D),V=function(e){a(i,e);var n=f(i);function i(e){var r,o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},c=o.location,a=void 0===c?A.location:c,s=o.threshold,u=void 0===s?A.threshold:s,h=o.distance,f=void 0===h?A.distance:h,l=o.includeMatches,d=void 0===l?A.includeMatches:l,v=o.findAllMatches,g=void 0===v?A.findAllMatches:v,y=o.minMatchCharLength,p=void 0===y?A.minMatchCharLength:y,m=o.isCaseSensitive,k=void 0===m?A.isCaseSensitive:m,M=o.ignoreLocation,b=void 0===M?A.ignoreLocation:M;return t(this,i),(r=n.call(this,e))._bitapSearch=new N(e,{location:a,threshold:u,distance:f,includeMatches:d,findAllMatches:g,minMatchCharLength:p,isCaseSensitive:k,ignoreLocation:b}),r}return r(i,[{key:"search",value:function(e){return this._bitapSearch.searchIn(e)}}],[{key:"type",get:function(){return"fuzzy"}},{key:"multiRegex",get:function(){return/^"(.*)"$/}},{key:"singleRegex",get:function(){return/^(.*)$/}}]),i}(D),B=function(e){a(i,e);var n=f(i);function i(e){return t(this,i),n.call(this,e)}return r(i,[{key:"search",value:function(e){for(var t,n=0,r=[],i=this.pattern.length;(t=e.indexOf(this.pattern,n))>-1;)n=t+i,r.push([t,n-1]);var o=!!r.length;return{isMatch:o,score:o?0:1,indices:r}}}],[{key:"type",get:function(){return"include"}},{key:"multiRegex",get:function(){return/^'"(.*)"$/}},{key:"singleRegex",get:function(){return/^'(.*)$/}}]),i}(D),G=[K,B,W,J,U,T,q,V],H=G.length,Q=/ +(?=([^\"]*\"[^\"]*\")*[^\"]*$)/;function X(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return e.split("|").map((function(e){for(var n=e.trim().split(Q).filter((function(e){return e&&!!e.trim()})),r=[],i=0,o=n.length;i1&&void 0!==arguments[1]?arguments[1]:{},i=r.isCaseSensitive,o=void 0===i?A.isCaseSensitive:i,c=r.includeMatches,a=void 0===c?A.includeMatches:c,s=r.minMatchCharLength,u=void 0===s?A.minMatchCharLength:s,h=r.ignoreLocation,f=void 0===h?A.ignoreLocation:h,l=r.findAllMatches,d=void 0===l?A.findAllMatches:l,v=r.location,g=void 0===v?A.location:v,y=r.threshold,p=void 0===y?A.threshold:y,m=r.distance,k=void 0===m?A.distance:m;t(this,e),this.query=null,this.options={isCaseSensitive:o,includeMatches:a,minMatchCharLength:u,findAllMatches:d,ignoreLocation:f,location:g,threshold:p,distance:k},this.pattern=o?n:n.toLowerCase(),this.query=X(this.pattern,this.options)}return r(e,[{key:"searchIn",value:function(e){var t=this.query;if(!t)return{isMatch:!1,score:1};var n=this.options,r=n.includeMatches;e=n.isCaseSensitive?e:e.toLowerCase();for(var i=0,o=[],c=0,a=0,s=t.length;a-1&&(n.refIndex=e.idx),t.matches.push(n)}}))}function le(e,t){t.score=e.score}function de(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},r=n.includeMatches,i=void 0===r?A.includeMatches:r,o=n.includeScore,c=void 0===o?A.includeScore:o,a=[];return i&&a.push(fe),c&&a.push(le),e.map((function(e){var n=e.idx,r={item:t[n],refIndex:n};return a.length&&a.forEach((function(t){t(e,r)})),r}))}var ve=function(){function e(n){var r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},i=arguments.length>2?arguments[2]:void 0;t(this,e),this.options=c({},A,{},r),this.options.useExtendedSearch,this._keyStore=new w(this.options.keys),this.setCollection(n,i)}return r(e,[{key:"setCollection",value:function(e,t){if(this._docs=e,t&&!(t instanceof E))throw new Error("Incorrect 'index' type");this._myIndex=t||$(this.options.keys,this._docs,{getFn:this.options.getFn})}},{key:"add",value:function(e){k(e)&&(this._docs.push(e),this._myIndex.add(e))}},{key:"remove",value:function(){for(var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:function(){return!1},t=[],n=0,r=this._docs.length;n1&&void 0!==arguments[1]?arguments[1]:{},n=t.limit,r=void 0===n?-1:n,i=this.options,o=i.includeMatches,c=i.includeScore,a=i.shouldSort,s=i.sortFn,u=i.ignoreFieldNorm,h=g(e)?g(this._docs[0])?this._searchStringList(e):this._searchObjectList(e):this._searchLogical(e);return he(h,{ignoreFieldNorm:u}),a&&h.sort(s),y(r)&&r>-1&&(h=h.slice(0,r)),de(h,this._docs,{includeMatches:o,includeScore:c})}},{key:"_searchStringList",value:function(e){var t=te(e,this.options),n=this._myIndex.records,r=[];return n.forEach((function(e){var n=e.v,i=e.i,o=e.n;if(k(n)){var c=t.searchIn(n),a=c.isMatch,s=c.score,u=c.indices;a&&r.push({item:n,idx:i,matches:[{score:s,value:n,norm:o,indices:u}]})}})),r}},{key:"_searchLogical",value:function(e){var t=this,n=function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},r=n.auto,i=void 0===r||r,o=function e(n){var r=Object.keys(n),o=ae(n);if(!o&&r.length>1&&!ce(n))return e(ue(n));if(se(n)){var c=o?n[ie]:r[0],a=o?n[oe]:n[c];if(!g(a))throw new Error(x(c));var s={keyId:j(c),pattern:a};return i&&(s.searcher=te(a,t)),s}var u={children:[],operator:r[0]};return r.forEach((function(t){var r=n[t];v(r)&&r.forEach((function(t){u.children.push(e(t))}))})),u};return ce(e)||(e=ue(e)),o(e)}(e,this.options),r=this._myIndex.records,i={},o=[];return r.forEach((function(e){var r=e.$,c=e.i;if(k(r)){var a=function e(n,r,i){if(!n.children){var o=n.keyId,c=n.searcher,a=t._findMatches({key:t._keyStore.get(o),value:t._myIndex.getValueForItemAtKeyId(r,o),searcher:c});return a&&a.length?[{idx:i,item:r,matches:a}]:[]}switch(n.operator){case ne:for(var s=[],u=0,h=n.children.length;u1&&void 0!==arguments[1]?arguments[1]:{},n=t.getFn,r=void 0===n?A.getFn:n,i=e.keys,o=e.records,c=new E({getFn:r});return c.setKeys(i),c.setIndexRecords(o),c},ve.config=A,function(){ee.push.apply(ee,arguments)}(Z),ve},"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self).Fuse=t(); \ No newline at end of file diff --git a/docs/documentation/sitespeed.io/scripting/scripts/third-party/hljs-line-num-original.js b/docs/documentation/sitespeed.io/scripting/scripts/third-party/hljs-line-num-original.js new file mode 100644 index 0000000000..9b8e18f77c --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/scripts/third-party/hljs-line-num-original.js @@ -0,0 +1,369 @@ +// jshint multistr:true + +(function (w, d) { + 'use strict'; + + var TABLE_NAME = 'hljs-ln', + LINE_NAME = 'hljs-ln-line', + CODE_BLOCK_NAME = 'hljs-ln-code', + NUMBERS_BLOCK_NAME = 'hljs-ln-numbers', + NUMBER_LINE_NAME = 'hljs-ln-n', + DATA_ATTR_NAME = 'data-line-number', + BREAK_LINE_REGEXP = /\r\n|\r|\n/g; + + if (w.hljs) { + w.hljs.initLineNumbersOnLoad = initLineNumbersOnLoad; + w.hljs.lineNumbersBlock = lineNumbersBlock; + w.hljs.lineNumbersValue = lineNumbersValue; + + addStyles(); + } else { + w.console.error('highlight.js not detected!'); + } + + function isHljsLnCodeDescendant(domElt) { + var curElt = domElt; + while (curElt) { + if (curElt.className && curElt.className.indexOf('hljs-ln-code') !== -1) { + return true; + } + curElt = curElt.parentNode; + } + return false; + } + + function getHljsLnTable(hljsLnDomElt) { + var curElt = hljsLnDomElt; + while (curElt.nodeName !== 'TABLE') { + curElt = curElt.parentNode; + } + return curElt; + } + + // Function to workaround a copy issue with Microsoft Edge. + // Due to hljs-ln wrapping the lines of code inside a element, + // itself wrapped inside a
 element, window.getSelection().toString()
+    // does not contain any line breaks. So we need to get them back using the
+    // rendered code in the DOM as reference.
+    function edgeGetSelectedCodeLines(selection) {
+        // current selected text without line breaks
+        var selectionText = selection.toString();
+
+        // get the 
' + + '' + + '' + + '', + [ + LINE_NAME, + NUMBERS_BLOCK_NAME, + NUMBER_LINE_NAME, + DATA_ATTR_NAME, + CODE_BLOCK_NAME, + i + options.startFrom, + lines[i].length > 0 ? lines[i] : ' ' + ]); + } + + return format('
element wrapping the first line of selected code + var tdAnchor = selection.anchorNode; + while (tdAnchor.nodeName !== 'TD') { + tdAnchor = tdAnchor.parentNode; + } + + // get the element wrapping the last line of selected code + var tdFocus = selection.focusNode; + while (tdFocus.nodeName !== 'TD') { + tdFocus = tdFocus.parentNode; + } + + // extract line numbers + var firstLineNumber = parseInt(tdAnchor.dataset.lineNumber); + var lastLineNumber = parseInt(tdFocus.dataset.lineNumber); + + // multi-lines copied case + if (firstLineNumber != lastLineNumber) { + + var firstLineText = tdAnchor.textContent; + var lastLineText = tdFocus.textContent; + + // if the selection was made backward, swap values + if (firstLineNumber > lastLineNumber) { + var tmp = firstLineNumber; + firstLineNumber = lastLineNumber; + lastLineNumber = tmp; + tmp = firstLineText; + firstLineText = lastLineText; + lastLineText = tmp; + } + + // discard not copied characters in first line + while (selectionText.indexOf(firstLineText) !== 0) { + firstLineText = firstLineText.slice(1); + } + + // discard not copied characters in last line + while (selectionText.lastIndexOf(lastLineText) === -1) { + lastLineText = lastLineText.slice(0, -1); + } + + // reconstruct and return the real copied text + var selectedText = firstLineText; + var hljsLnTable = getHljsLnTable(tdAnchor); + for (var i = firstLineNumber + 1 ; i < lastLineNumber ; ++i) { + var codeLineSel = format('.{0}[{1}="{2}"]', [CODE_BLOCK_NAME, DATA_ATTR_NAME, i]); + var codeLineElt = hljsLnTable.querySelector(codeLineSel); + selectedText += '\n' + codeLineElt.textContent; + } + selectedText += '\n' + lastLineText; + return selectedText; + // single copied line case + } else { + return selectionText; + } + } + + // ensure consistent code copy/paste behavior across all browsers + // (see https://github.com/wcoder/highlightjs-line-numbers.js/issues/51) + document.addEventListener('copy', function(e) { + // get current selection + var selection = window.getSelection(); + // override behavior when one wants to copy line of codes + if (isHljsLnCodeDescendant(selection.anchorNode)) { + var selectionText; + // workaround an issue with Microsoft Edge as copied line breaks + // are removed otherwise from the selection string + if (window.navigator.userAgent.indexOf('Edge') !== -1) { + selectionText = edgeGetSelectedCodeLines(selection); + } else { + // other browsers can directly use the selection string + selectionText = selection.toString(); + } + e.clipboardData.setData( + 'text/plain', + selectionText + .replace(/(^\t)/gm, '') + ); + e.preventDefault(); + } + }); + + function addStyles () { + var css = d.createElement('style'); + css.type = 'text/css'; + css.innerHTML = format( + '.{0}{border-collapse:collapse}' + + '.{0} td{padding:0}' + + '.{1}:before{content:attr({2})}', + [ + TABLE_NAME, + NUMBER_LINE_NAME, + DATA_ATTR_NAME + ]); + d.getElementsByTagName('head')[0].appendChild(css); + } + + function initLineNumbersOnLoad (options) { + if (d.readyState === 'interactive' || d.readyState === 'complete') { + documentReady(options); + } else { + w.addEventListener('DOMContentLoaded', function () { + documentReady(options); + }); + } + } + + function documentReady (options) { + try { + var blocks = d.querySelectorAll('code.hljs,code.nohighlight'); + + for (var i in blocks) { + if (blocks.hasOwnProperty(i)) { + if (!isPluginDisabledForBlock(blocks[i])) { + lineNumbersBlock(blocks[i], options); + } + } + } + } catch (e) { + w.console.error('LineNumbers error: ', e); + } + } + + function isPluginDisabledForBlock(element) { + return element.classList.contains('nohljsln'); + } + + function lineNumbersBlock (element, options) { + if (typeof element !== 'object') return; + + async(function () { + element.innerHTML = lineNumbersInternal(element, options); + }); + } + + function lineNumbersValue (value, options) { + if (typeof value !== 'string') return; + + var element = document.createElement('code') + element.innerHTML = value + + return lineNumbersInternal(element, options); + } + + function lineNumbersInternal (element, options) { + + var internalOptions = mapOptions(element, options); + + duplicateMultilineNodes(element); + + return addLineNumbersBlockFor(element.innerHTML, internalOptions); + } + + function addLineNumbersBlockFor (inputHtml, options) { + var lines = getLines(inputHtml); + + // if last line contains only carriage return remove it + if (lines[lines.length-1].trim() === '') { + lines.pop(); + } + + if (lines.length > 1 || options.singleLine) { + var html = ''; + + for (var i = 0, l = lines.length; i < l; i++) { + html += format( + '
' + + '' + + '{6}' + + '
{1}
', [ TABLE_NAME, html ]); + } + + return inputHtml; + } + + /** + * @param {HTMLElement} element Code block. + * @param {Object} options External API options. + * @returns {Object} Internal API options. + */ + function mapOptions (element, options) { + options = options || {}; + return { + singleLine: getSingleLineOption(options), + startFrom: getStartFromOption(element, options) + }; + } + + function getSingleLineOption (options) { + var defaultValue = false; + if (!!options.singleLine) { + return options.singleLine; + } + return defaultValue; + } + + function getStartFromOption (element, options) { + var defaultValue = 1; + var startFrom = defaultValue; + + if (isFinite(options.startFrom)) { + startFrom = options.startFrom; + } + + // can be overridden because local option is priority + var value = getAttribute(element, 'data-ln-start-from'); + if (value !== null) { + startFrom = toNumber(value, defaultValue); + } + + return startFrom; + } + + /** + * Recursive method for fix multi-line elements implementation in highlight.js + * Doing deep passage on child nodes. + * @param {HTMLElement} element + */ + function duplicateMultilineNodes (element) { + var nodes = element.childNodes; + for (var node in nodes) { + if (nodes.hasOwnProperty(node)) { + var child = nodes[node]; + if (getLinesCount(child.textContent) > 0) { + if (child.childNodes.length > 0) { + duplicateMultilineNodes(child); + } else { + duplicateMultilineNode(child.parentNode); + } + } + } + } + } + + /** + * Method for fix multi-line elements implementation in highlight.js + * @param {HTMLElement} element + */ + function duplicateMultilineNode (element) { + var className = element.className; + + if ( ! /hljs-/.test(className)) return; + + var lines = getLines(element.innerHTML); + + for (var i = 0, result = ''; i < lines.length; i++) { + var lineText = lines[i].length > 0 ? lines[i] : ' '; + result += format('{1}\n', [ className, lineText ]); + } + + element.innerHTML = result.trim(); + } + + function getLines (text) { + if (text.length === 0) return []; + return text.split(BREAK_LINE_REGEXP); + } + + function getLinesCount (text) { + return (text.trim().match(BREAK_LINE_REGEXP) || []).length; + } + + /// + /// HELPERS + /// + + function async (func) { + w.setTimeout(func, 0); + } + + /** + * {@link https://wcoder.github.io/notes/string-format-for-string-formating-in-javascript} + * @param {string} format + * @param {array} args + */ + function format (format, args) { + return format.replace(/\{(\d+)\}/g, function(m, n){ + return args[n] !== undefined ? args[n] : m; + }); + } + + /** + * @param {HTMLElement} element Code block. + * @param {String} attrName Attribute name. + * @returns {String} Attribute value or empty. + */ + function getAttribute (element, attrName) { + return element.hasAttribute(attrName) ? element.getAttribute(attrName) : null; + } + + /** + * @param {String} str Source string. + * @param {Number} fallback Fallback value. + * @returns Parsed number or fallback value. + */ + function toNumber (str, fallback) { + if (!str) return fallback; + var number = Number(str); + return isFinite(number) ? number : fallback; + } + +}(window, document)); diff --git a/docs/documentation/sitespeed.io/scripting/scripts/third-party/hljs-line-num.js b/docs/documentation/sitespeed.io/scripting/scripts/third-party/hljs-line-num.js new file mode 100644 index 0000000000..facdf6bed4 --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/scripts/third-party/hljs-line-num.js @@ -0,0 +1 @@ +!function(r,o){"use strict";var e,l="hljs-ln",s="hljs-ln-line",f="hljs-ln-code",c="hljs-ln-numbers",u="hljs-ln-n",h="data-line-number",n=/\r\n|\r|\n/g;function t(e){for(var n=e.toString(),t=e.anchorNode;"TD"!==t.nodeName;)t=t.parentNode;for(var r=e.focusNode;"TD"!==r.nodeName;)r=r.parentNode;var e=parseInt(t.dataset.lineNumber),o=parseInt(r.dataset.lineNumber);if(e==o)return n;var a,i=t.textContent,l=r.textContent;for(o{6}',[s,c,u,h,f,a+t.startFrom,0{1}',[l,o])}return e}function m(e){var n=e.className;if(/hljs-/.test(n)){for(var t=g(e.innerHTML),r=0,o="";r{1}\n',[n,0/g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } + + /** + * performs a shallow merge of multiple objects into one + * + * @template T + * @param {T} original + * @param {Record[]} objects + * @returns {T} a single new object + */ + function inherit$1(original, ...objects) { + /** @type Record */ + const result = Object.create(null); + + for (const key in original) { + result[key] = original[key]; + } + objects.forEach(function (obj) { + for (const key in obj) { + result[key] = obj[key]; + } + }); + return /** @type {T} */ (result); + } + + /** + * @typedef {object} Renderer + * @property {(text: string) => void} addText + * @property {(node: Node) => void} openNode + * @property {(node: Node) => void} closeNode + * @property {() => string} value + */ + + /** @typedef {{kind?: string, sublanguage?: boolean}} Node */ + /** @typedef {{walk: (r: Renderer) => void}} Tree */ + /** */ + + const SPAN_CLOSE = ''; + + /** + * Determines if a node needs to be wrapped in + * + * @param {Node} node */ + const emitsWrappingTags = (node) => { + return !!node.kind; + }; + + /** + * + * @param {string} name + * @param {{prefix:string}} options + */ + const expandScopeName = (name, { prefix }) => { + if (name.includes(".")) { + const pieces = name.split("."); + return [ + `${prefix}${pieces.shift()}`, + ...(pieces.map((x, i) => `${x}${"_".repeat(i + 1)}`)) + ].join(" "); + } + return `${prefix}${name}`; + }; + + /** @type {Renderer} */ + class HTMLRenderer { + /** + * Creates a new HTMLRenderer + * + * @param {Tree} parseTree - the parse tree (must support `walk` API) + * @param {{classPrefix: string}} options + */ + constructor(parseTree, options) { + this.buffer = ""; + this.classPrefix = options.classPrefix; + parseTree.walk(this); + } + + /** + * Adds texts to the output stream + * + * @param {string} text */ + addText(text) { + this.buffer += escapeHTML(text); + } + + /** + * Adds a node open to the output stream (if needed) + * + * @param {Node} node */ + openNode(node) { + if (!emitsWrappingTags(node)) return; + + let scope = node.kind; + if (node.sublanguage) { + scope = `language-${scope}`; + } else { + scope = expandScopeName(scope, { prefix: this.classPrefix }); + } + this.span(scope); + } + + /** + * Adds a node close to the output stream (if needed) + * + * @param {Node} node */ + closeNode(node) { + if (!emitsWrappingTags(node)) return; + + this.buffer += SPAN_CLOSE; + } + + /** + * returns the accumulated buffer + */ + value() { + return this.buffer; + } + + // helpers + + /** + * Builds a span element + * + * @param {string} className */ + span(className) { + this.buffer += ``; + } + } + + /** @typedef {{kind?: string, sublanguage?: boolean, children: Node[]} | string} Node */ + /** @typedef {{kind?: string, sublanguage?: boolean, children: Node[]} } DataNode */ + /** @typedef {import('highlight.js').Emitter} Emitter */ + /** */ + + class TokenTree { + constructor() { + /** @type DataNode */ + this.rootNode = { children: [] }; + this.stack = [this.rootNode]; + } + + get top() { + return this.stack[this.stack.length - 1]; + } + + get root() { return this.rootNode; } + + /** @param {Node} node */ + add(node) { + this.top.children.push(node); + } + + /** @param {string} kind */ + openNode(kind) { + /** @type Node */ + const node = { kind, children: [] }; + this.add(node); + this.stack.push(node); + } + + closeNode() { + if (this.stack.length > 1) { + return this.stack.pop(); + } + // eslint-disable-next-line no-undefined + return undefined; + } + + closeAllNodes() { + while (this.closeNode()); + } + + toJSON() { + return JSON.stringify(this.rootNode, null, 4); + } + + /** + * @typedef { import("./html_renderer").Renderer } Renderer + * @param {Renderer} builder + */ + walk(builder) { + // this does not + return this.constructor._walk(builder, this.rootNode); + // this works + // return TokenTree._walk(builder, this.rootNode); + } + + /** + * @param {Renderer} builder + * @param {Node} node + */ + static _walk(builder, node) { + if (typeof node === "string") { + builder.addText(node); + } else if (node.children) { + builder.openNode(node); + node.children.forEach((child) => this._walk(builder, child)); + builder.closeNode(node); + } + return builder; + } + + /** + * @param {Node} node + */ + static _collapse(node) { + if (typeof node === "string") return; + if (!node.children) return; + + if (node.children.every(el => typeof el === "string")) { + // node.text = node.children.join(""); + // delete node.children; + node.children = [node.children.join("")]; + } else { + node.children.forEach((child) => { + TokenTree._collapse(child); + }); + } + } + } + + /** + Currently this is all private API, but this is the minimal API necessary + that an Emitter must implement to fully support the parser. + + Minimal interface: + + - addKeyword(text, kind) + - addText(text) + - addSublanguage(emitter, subLanguageName) + - finalize() + - openNode(kind) + - closeNode() + - closeAllNodes() + - toHTML() + + */ + + /** + * @implements {Emitter} + */ + class TokenTreeEmitter extends TokenTree { + /** + * @param {*} options + */ + constructor(options) { + super(); + this.options = options; + } + + /** + * @param {string} text + * @param {string} kind + */ + addKeyword(text, kind) { + if (text === "") { return; } + + this.openNode(kind); + this.addText(text); + this.closeNode(); + } + + /** + * @param {string} text + */ + addText(text) { + if (text === "") { return; } + + this.add(text); + } + + /** + * @param {Emitter & {root: DataNode}} emitter + * @param {string} name + */ + addSublanguage(emitter, name) { + /** @type DataNode */ + const node = emitter.root; + node.kind = name; + node.sublanguage = true; + this.add(node); + } + + toHTML() { + const renderer = new HTMLRenderer(this, this.options); + return renderer.value(); + } + + finalize() { + return true; + } + } + + /** + * @param {string} value + * @returns {RegExp} + * */ + + /** + * @param {RegExp | string } re + * @returns {string} + */ + function source(re) { + if (!re) return null; + if (typeof re === "string") return re; + + return re.source; + } + + /** + * @param {RegExp | string } re + * @returns {string} + */ + function lookahead(re) { + return concat('(?=', re, ')'); + } + + /** + * @param {RegExp | string } re + * @returns {string} + */ + function optional(re) { + return concat('(?:', re, ')?'); + } + + /** + * @param {...(RegExp | string) } args + * @returns {string} + */ + function concat(...args) { + const joined = args.map((x) => source(x)).join(""); + return joined; + } + + function stripOptionsFromArgs(args) { + const opts = args[args.length - 1]; + + if (typeof opts === 'object' && opts.constructor === Object) { + args.splice(args.length - 1, 1); + return opts; + } else { + return {}; + } + } + + /** + * Any of the passed expresssions may match + * + * Creates a huge this | this | that | that match + * @param {(RegExp | string)[] } args + * @returns {string} + */ + function either(...args) { + const opts = stripOptionsFromArgs(args); + const joined = '(' + + (opts.capture ? "" : "?:") + + args.map((x) => source(x)).join("|") + ")"; + return joined; + } + + /** + * @param {RegExp} re + * @returns {number} + */ + function countMatchGroups(re) { + return (new RegExp(re.toString() + '|')).exec('').length - 1; + } + + /** + * Does lexeme start with a regular expression match at the beginning + * @param {RegExp} re + * @param {string} lexeme + */ + function startsWith(re, lexeme) { + const match = re && re.exec(lexeme); + return match && match.index === 0; + } + + // BACKREF_RE matches an open parenthesis or backreference. To avoid + // an incorrect parse, it additionally matches the following: + // - [...] elements, where the meaning of parentheses and escapes change + // - other escape sequences, so we do not misparse escape sequences as + // interesting elements + // - non-matching or lookahead parentheses, which do not capture. These + // follow the '(' with a '?'. + const BACKREF_RE = /\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./; + + // **INTERNAL** Not intended for outside usage + // join logically computes regexps.join(separator), but fixes the + // backreferences so they continue to match. + // it also places each individual regular expression into it's own + // match group, keeping track of the sequencing of those match groups + // is currently an exercise for the caller. :-) + /** + * @param {(string | RegExp)[]} regexps + * @param {{joinWith: string}} opts + * @returns {string} + */ + function _rewriteBackreferences(regexps, { joinWith }) { + let numCaptures = 0; + + return regexps.map((regex) => { + numCaptures += 1; + const offset = numCaptures; + let re = source(regex); + let out = ''; + + while (re.length > 0) { + const match = BACKREF_RE.exec(re); + if (!match) { + out += re; + break; + } + out += re.substring(0, match.index); + re = re.substring(match.index + match[0].length); + if (match[0][0] === '\\' && match[1]) { + // Adjust the backreference. + out += '\\' + String(Number(match[1]) + offset); + } else { + out += match[0]; + if (match[0] === '(') { + numCaptures++; + } + } + } + return out; + }).map(re => `(${re})`).join(joinWith); + } + + /** @typedef {import('highlight.js').Mode} Mode */ + /** @typedef {import('highlight.js').ModeCallback} ModeCallback */ + + // Common regexps + const MATCH_NOTHING_RE = /\b\B/; + const IDENT_RE$1 = '[a-zA-Z]\\w*'; + const UNDERSCORE_IDENT_RE = '[a-zA-Z_]\\w*'; + const NUMBER_RE = '\\b\\d+(\\.\\d+)?'; + const C_NUMBER_RE = '(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)'; // 0x..., 0..., decimal, float + const BINARY_NUMBER_RE = '\\b(0b[01]+)'; // 0b... + const RE_STARTERS_RE = '!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~'; + + /** + * @param { Partial & {binary?: string | RegExp} } opts + */ + const SHEBANG = (opts = {}) => { + const beginShebang = /^#![ ]*\//; + if (opts.binary) { + opts.begin = concat( + beginShebang, + /.*\b/, + opts.binary, + /\b.*/); + } + return inherit$1({ + scope: 'meta', + begin: beginShebang, + end: /$/, + relevance: 0, + /** @type {ModeCallback} */ + "on:begin": (m, resp) => { + if (m.index !== 0) resp.ignoreMatch(); + } + }, opts); + }; + + // Common modes + const BACKSLASH_ESCAPE = { + begin: '\\\\[\\s\\S]', relevance: 0 + }; + const APOS_STRING_MODE = { + scope: 'string', + begin: '\'', + end: '\'', + illegal: '\\n', + contains: [BACKSLASH_ESCAPE] + }; + const QUOTE_STRING_MODE = { + scope: 'string', + begin: '"', + end: '"', + illegal: '\\n', + contains: [BACKSLASH_ESCAPE] + }; + const PHRASAL_WORDS_MODE = { + begin: /\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/ + }; + /** + * Creates a comment mode + * + * @param {string | RegExp} begin + * @param {string | RegExp} end + * @param {Mode | {}} [modeOptions] + * @returns {Partial} + */ + const COMMENT = function (begin, end, modeOptions = {}) { + const mode = inherit$1( + { + scope: 'comment', + begin, + end, + contains: [] + }, + modeOptions + ); + mode.contains.push({ + scope: 'doctag', + // hack to avoid the space from being included. the space is necessary to + // match here to prevent the plain text rule below from gobbling up doctags + begin: '[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)', + end: /(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/, + excludeBegin: true, + relevance: 0 + }); + const ENGLISH_WORD = either( + // list of common 1 and 2 letter words in English + "I", + "a", + "is", + "so", + "us", + "to", + "at", + "if", + "in", + "it", + "on", + // note: this is not an exhaustive list of contractions, just popular ones + /[A-Za-z]+['](d|ve|re|ll|t|s|n)/, // contractions - can't we'd they're let's, etc + /[A-Za-z]+[-][a-z]+/, // `no-way`, etc. + /[A-Za-z][a-z]{2,}/ // allow capitalized words at beginning of sentences + ); + // looking like plain text, more likely to be a comment + mode.contains.push( + { + // TODO: how to include ", (, ) without breaking grammars that use these for + // comment delimiters? + // begin: /[ ]+([()"]?([A-Za-z'-]{3,}|is|a|I|so|us|[tT][oO]|at|if|in|it|on)[.]?[()":]?([.][ ]|[ ]|\))){3}/ + // --- + + // this tries to find sequences of 3 english words in a row (without any + // "programming" type syntax) this gives us a strong signal that we've + // TRULY found a comment - vs perhaps scanning with the wrong language. + // It's possible to find something that LOOKS like the start of the + // comment - but then if there is no readable text - good chance it is a + // false match and not a comment. + // + // for a visual example please see: + // https://github.com/highlightjs/highlight.js/issues/2827 + + begin: concat( + /[ ]+/, // necessary to prevent us gobbling up doctags like /* @author Bob Mcgill */ + '(', + ENGLISH_WORD, + /[.]?[:]?([.][ ]|[ ])/, + '){3}') // look for 3 words in a row + } + ); + return mode; + }; + const C_LINE_COMMENT_MODE = COMMENT('//', '$'); + const C_BLOCK_COMMENT_MODE = COMMENT('/\\*', '\\*/'); + const HASH_COMMENT_MODE = COMMENT('#', '$'); + const NUMBER_MODE = { + scope: 'number', + begin: NUMBER_RE, + relevance: 0 + }; + const C_NUMBER_MODE = { + scope: 'number', + begin: C_NUMBER_RE, + relevance: 0 + }; + const BINARY_NUMBER_MODE = { + scope: 'number', + begin: BINARY_NUMBER_RE, + relevance: 0 + }; + const REGEXP_MODE = { + // this outer rule makes sure we actually have a WHOLE regex and not simply + // an expression such as: + // + // 3 / something + // + // (which will then blow up when regex's `illegal` sees the newline) + begin: /(?=\/[^/\n]*\/)/, + contains: [{ + scope: 'regexp', + begin: /\//, + end: /\/[gimuy]*/, + illegal: /\n/, + contains: [ + BACKSLASH_ESCAPE, + { + begin: /\[/, + end: /\]/, + relevance: 0, + contains: [BACKSLASH_ESCAPE] + } + ] + }] + }; + const TITLE_MODE = { + scope: 'title', + begin: IDENT_RE$1, + relevance: 0 + }; + const UNDERSCORE_TITLE_MODE = { + scope: 'title', + begin: UNDERSCORE_IDENT_RE, + relevance: 0 + }; + const METHOD_GUARD = { + // excludes method names from keyword processing + begin: '\\.\\s*' + UNDERSCORE_IDENT_RE, + relevance: 0 + }; + + /** + * Adds end same as begin mechanics to a mode + * + * Your mode must include at least a single () match group as that first match + * group is what is used for comparison + * @param {Partial} mode + */ + const END_SAME_AS_BEGIN = function (mode) { + return Object.assign(mode, + { + /** @type {ModeCallback} */ + 'on:begin': (m, resp) => { resp.data._beginMatch = m[1]; }, + /** @type {ModeCallback} */ + 'on:end': (m, resp) => { if (resp.data._beginMatch !== m[1]) resp.ignoreMatch(); } + }); + }; + + var MODES$1 = /*#__PURE__*/Object.freeze({ + __proto__: null, + MATCH_NOTHING_RE: MATCH_NOTHING_RE, + IDENT_RE: IDENT_RE$1, + UNDERSCORE_IDENT_RE: UNDERSCORE_IDENT_RE, + NUMBER_RE: NUMBER_RE, + C_NUMBER_RE: C_NUMBER_RE, + BINARY_NUMBER_RE: BINARY_NUMBER_RE, + RE_STARTERS_RE: RE_STARTERS_RE, + SHEBANG: SHEBANG, + BACKSLASH_ESCAPE: BACKSLASH_ESCAPE, + APOS_STRING_MODE: APOS_STRING_MODE, + QUOTE_STRING_MODE: QUOTE_STRING_MODE, + PHRASAL_WORDS_MODE: PHRASAL_WORDS_MODE, + COMMENT: COMMENT, + C_LINE_COMMENT_MODE: C_LINE_COMMENT_MODE, + C_BLOCK_COMMENT_MODE: C_BLOCK_COMMENT_MODE, + HASH_COMMENT_MODE: HASH_COMMENT_MODE, + NUMBER_MODE: NUMBER_MODE, + C_NUMBER_MODE: C_NUMBER_MODE, + BINARY_NUMBER_MODE: BINARY_NUMBER_MODE, + REGEXP_MODE: REGEXP_MODE, + TITLE_MODE: TITLE_MODE, + UNDERSCORE_TITLE_MODE: UNDERSCORE_TITLE_MODE, + METHOD_GUARD: METHOD_GUARD, + END_SAME_AS_BEGIN: END_SAME_AS_BEGIN + }); + + /** + @typedef {import('highlight.js').CallbackResponse} CallbackResponse + @typedef {import('highlight.js').CompilerExt} CompilerExt + */ + + // Grammar extensions / plugins + // See: https://github.com/highlightjs/highlight.js/issues/2833 + + // Grammar extensions allow "syntactic sugar" to be added to the grammar modes + // without requiring any underlying changes to the compiler internals. + + // `compileMatch` being the perfect small example of now allowing a grammar + // author to write `match` when they desire to match a single expression rather + // than being forced to use `begin`. The extension then just moves `match` into + // `begin` when it runs. Ie, no features have been added, but we've just made + // the experience of writing (and reading grammars) a little bit nicer. + + // ------ + + // TODO: We need negative look-behind support to do this properly + /** + * Skip a match if it has a preceding dot + * + * This is used for `beginKeywords` to prevent matching expressions such as + * `bob.keyword.do()`. The mode compiler automatically wires this up as a + * special _internal_ 'on:begin' callback for modes with `beginKeywords` + * @param {RegExpMatchArray} match + * @param {CallbackResponse} response + */ + function skipIfHasPrecedingDot(match, response) { + const before = match.input[match.index - 1]; + if (before === ".") { + response.ignoreMatch(); + } + } + + /** + * + * @type {CompilerExt} + */ + function scopeClassName(mode, _parent) { + // eslint-disable-next-line no-undefined + if (mode.className !== undefined) { + mode.scope = mode.className; + delete mode.className; + } + } + + /** + * `beginKeywords` syntactic sugar + * @type {CompilerExt} + */ + function beginKeywords(mode, parent) { + if (!parent) return; + if (!mode.beginKeywords) return; + + // for languages with keywords that include non-word characters checking for + // a word boundary is not sufficient, so instead we check for a word boundary + // or whitespace - this does no harm in any case since our keyword engine + // doesn't allow spaces in keywords anyways and we still check for the boundary + // first + mode.begin = '\\b(' + mode.beginKeywords.split(' ').join('|') + ')(?!\\.)(?=\\b|\\s)'; + mode.__beforeBegin = skipIfHasPrecedingDot; + mode.keywords = mode.keywords || mode.beginKeywords; + delete mode.beginKeywords; + + // prevents double relevance, the keywords themselves provide + // relevance, the mode doesn't need to double it + // eslint-disable-next-line no-undefined + if (mode.relevance === undefined) mode.relevance = 0; + } + + /** + * Allow `illegal` to contain an array of illegal values + * @type {CompilerExt} + */ + function compileIllegal(mode, _parent) { + if (!Array.isArray(mode.illegal)) return; + + mode.illegal = either(...mode.illegal); + } + + /** + * `match` to match a single expression for readability + * @type {CompilerExt} + */ + function compileMatch(mode, _parent) { + if (!mode.match) return; + if (mode.begin || mode.end) throw new Error("begin & end are not supported with match"); + + mode.begin = mode.match; + delete mode.match; + } + + /** + * provides the default 1 relevance to all modes + * @type {CompilerExt} + */ + function compileRelevance(mode, _parent) { + // eslint-disable-next-line no-undefined + if (mode.relevance === undefined) mode.relevance = 1; + } + + // allow beforeMatch to act as a "qualifier" for the match + // the full match begin must be [beforeMatch][begin] + const beforeMatchExt = (mode, parent) => { + if (!mode.beforeMatch) return; + // starts conflicts with endsParent which we need to make sure the child + // rule is not matched multiple times + if (mode.starts) throw new Error("beforeMatch cannot be used with starts"); + + const originalMode = Object.assign({}, mode); + Object.keys(mode).forEach((key) => { delete mode[key]; }); + + mode.keywords = originalMode.keywords; + mode.begin = concat(originalMode.beforeMatch, lookahead(originalMode.begin)); + mode.starts = { + relevance: 0, + contains: [ + Object.assign(originalMode, { endsParent: true }) + ] + }; + mode.relevance = 0; + + delete originalMode.beforeMatch; + }; + + // keywords that should have no default relevance value + const COMMON_KEYWORDS = [ + 'of', + 'and', + 'for', + 'in', + 'not', + 'or', + 'if', + 'then', + 'parent', // common variable name + 'list', // common variable name + 'value' // common variable name + ]; + + const DEFAULT_KEYWORD_SCOPE = "keyword"; + + /** + * Given raw keywords from a language definition, compile them. + * + * @param {string | Record | Array} rawKeywords + * @param {boolean} caseInsensitive + */ + function compileKeywords(rawKeywords, caseInsensitive, scopeName = DEFAULT_KEYWORD_SCOPE) { + /** @type KeywordDict */ + const compiledKeywords = Object.create(null); + + // input can be a string of keywords, an array of keywords, or a object with + // named keys representing scopeName (which can then point to a string or array) + if (typeof rawKeywords === 'string') { + compileList(scopeName, rawKeywords.split(" ")); + } else if (Array.isArray(rawKeywords)) { + compileList(scopeName, rawKeywords); + } else { + Object.keys(rawKeywords).forEach(function (scopeName) { + // collapse all our objects back into the parent object + Object.assign( + compiledKeywords, + compileKeywords(rawKeywords[scopeName], caseInsensitive, scopeName) + ); + }); + } + return compiledKeywords; + + // --- + + /** + * Compiles an individual list of keywords + * + * Ex: "for if when while|5" + * + * @param {string} scopeName + * @param {Array} keywordList + */ + function compileList(scopeName, keywordList) { + if (caseInsensitive) { + keywordList = keywordList.map(x => x.toLowerCase()); + } + keywordList.forEach(function (keyword) { + const pair = keyword.split('|'); + compiledKeywords[pair[0]] = [scopeName, scoreForKeyword(pair[0], pair[1])]; + }); + } + } + + /** + * Returns the proper score for a given keyword + * + * Also takes into account comment keywords, which will be scored 0 UNLESS + * another score has been manually assigned. + * @param {string} keyword + * @param {string} [providedScore] + */ + function scoreForKeyword(keyword, providedScore) { + // manual scores always win over common keywords + // so you can force a score of 1 if you really insist + if (providedScore) { + return Number(providedScore); + } + + return commonKeyword(keyword) ? 0 : 1; + } + + /** + * Determines if a given keyword is common or not + * + * @param {string} keyword */ + function commonKeyword(keyword) { + return COMMON_KEYWORDS.includes(keyword.toLowerCase()); + } + + /* + + For the reasoning behind this please see: + https://github.com/highlightjs/highlight.js/issues/2880#issuecomment-747275419 + + */ + + /** + * @type {Record} + */ + const seenDeprecations = {}; + + /** + * @param {string} message + */ + const error = (message) => { + console.error(message); + }; + + /** + * @param {string} message + * @param {any} args + */ + const warn = (message, ...args) => { + console.log(`WARN: ${message}`, ...args); + }; + + /** + * @param {string} version + * @param {string} message + */ + const deprecated = (version, message) => { + if (seenDeprecations[`${version}/${message}`]) return; + + console.log(`Deprecated as of ${version}. ${message}`); + seenDeprecations[`${version}/${message}`] = true; + }; + + /* eslint-disable no-throw-literal */ + + /** + @typedef {import('highlight.js').CompiledMode} CompiledMode + */ + + const MultiClassError = new Error(); + + /** + * Renumbers labeled scope names to account for additional inner match + * groups that otherwise would break everything. + * + * Lets say we 3 match scopes: + * + * { 1 => ..., 2 => ..., 3 => ... } + * + * So what we need is a clean match like this: + * + * (a)(b)(c) => [ "a", "b", "c" ] + * + * But this falls apart with inner match groups: + * + * (a)(((b)))(c) => ["a", "b", "b", "b", "c" ] + * + * Our scopes are now "out of alignment" and we're repeating `b` 3 times. + * What needs to happen is the numbers are remapped: + * + * { 1 => ..., 2 => ..., 5 => ... } + * + * We also need to know that the ONLY groups that should be output + * are 1, 2, and 5. This function handles this behavior. + * + * @param {CompiledMode} mode + * @param {Array} regexes + * @param {{key: "beginScope"|"endScope"}} opts + */ + function remapScopeNames(mode, regexes, { key }) { + let offset = 0; + const scopeNames = mode[key]; + /** @type Record */ + const emit = {}; + /** @type Record */ + const positions = {}; + + for (let i = 1; i <= regexes.length; i++) { + positions[i + offset] = scopeNames[i]; + emit[i + offset] = true; + offset += countMatchGroups(regexes[i - 1]); + } + // we use _emit to keep track of which match groups are "top-level" to avoid double + // output from inside match groups + mode[key] = positions; + mode[key]._emit = emit; + mode[key]._multi = true; + } + + /** + * @param {CompiledMode} mode + */ + function beginMultiClass(mode) { + if (!Array.isArray(mode.begin)) return; + + if (mode.skip || mode.excludeBegin || mode.returnBegin) { + error("skip, excludeBegin, returnBegin not compatible with beginScope: {}"); + throw MultiClassError; + } + + if (typeof mode.beginScope !== "object" || mode.beginScope === null) { + error("beginScope must be object"); + throw MultiClassError; + } + + remapScopeNames(mode, mode.begin, { key: "beginScope" }); + mode.begin = _rewriteBackreferences(mode.begin, { joinWith: "" }); + } + + /** + * @param {CompiledMode} mode + */ + function endMultiClass(mode) { + if (!Array.isArray(mode.end)) return; + + if (mode.skip || mode.excludeEnd || mode.returnEnd) { + error("skip, excludeEnd, returnEnd not compatible with endScope: {}"); + throw MultiClassError; + } + + if (typeof mode.endScope !== "object" || mode.endScope === null) { + error("endScope must be object"); + throw MultiClassError; + } + + remapScopeNames(mode, mode.end, { key: "endScope" }); + mode.end = _rewriteBackreferences(mode.end, { joinWith: "" }); + } + + /** + * this exists only to allow `scope: {}` to be used beside `match:` + * Otherwise `beginScope` would necessary and that would look weird + + { + match: [ /def/, /\w+/ ] + scope: { 1: "keyword" , 2: "title" } + } + + * @param {CompiledMode} mode + */ + function scopeSugar(mode) { + if (mode.scope && typeof mode.scope === "object" && mode.scope !== null) { + mode.beginScope = mode.scope; + delete mode.scope; + } + } + + /** + * @param {CompiledMode} mode + */ + function MultiClass(mode) { + scopeSugar(mode); + + if (typeof mode.beginScope === "string") { + mode.beginScope = { _wrap: mode.beginScope }; + } + if (typeof mode.endScope === "string") { + mode.endScope = { _wrap: mode.endScope }; + } + + beginMultiClass(mode); + endMultiClass(mode); + } + + /** + @typedef {import('highlight.js').Mode} Mode + @typedef {import('highlight.js').CompiledMode} CompiledMode + @typedef {import('highlight.js').Language} Language + @typedef {import('highlight.js').HLJSPlugin} HLJSPlugin + @typedef {import('highlight.js').CompiledLanguage} CompiledLanguage + */ + + // compilation + + /** + * Compiles a language definition result + * + * Given the raw result of a language definition (Language), compiles this so + * that it is ready for highlighting code. + * @param {Language} language + * @returns {CompiledLanguage} + */ + function compileLanguage(language) { + /** + * Builds a regex with the case sensitivity of the current language + * + * @param {RegExp | string} value + * @param {boolean} [global] + */ + function langRe(value, global) { + return new RegExp( + source(value), + 'm' + (language.case_insensitive ? 'i' : '') + (global ? 'g' : '') + ); + } + + /** + Stores multiple regular expressions and allows you to quickly search for + them all in a string simultaneously - returning the first match. It does + this by creating a huge (a|b|c) regex - each individual item wrapped with () + and joined by `|` - using match groups to track position. When a match is + found checking which position in the array has content allows us to figure + out which of the original regexes / match groups triggered the match. + + The match object itself (the result of `Regex.exec`) is returned but also + enhanced by merging in any meta-data that was registered with the regex. + This is how we keep track of which mode matched, and what type of rule + (`illegal`, `begin`, end, etc). + */ + class MultiRegex { + constructor() { + this.matchIndexes = {}; + // @ts-ignore + this.regexes = []; + this.matchAt = 1; + this.position = 0; + } + + // @ts-ignore + addRule(re, opts) { + opts.position = this.position++; + // @ts-ignore + this.matchIndexes[this.matchAt] = opts; + this.regexes.push([opts, re]); + this.matchAt += countMatchGroups(re) + 1; + } + + compile() { + if (this.regexes.length === 0) { + // avoids the need to check length every time exec is called + // @ts-ignore + this.exec = () => null; + } + const terminators = this.regexes.map(el => el[1]); + this.matcherRe = langRe(_rewriteBackreferences(terminators, { joinWith: '|' }), true); + this.lastIndex = 0; + } + + /** @param {string} s */ + exec(s) { + this.matcherRe.lastIndex = this.lastIndex; + const match = this.matcherRe.exec(s); + if (!match) { return null; } + + // eslint-disable-next-line no-undefined + const i = match.findIndex((el, i) => i > 0 && el !== undefined); + // @ts-ignore + const matchData = this.matchIndexes[i]; + // trim off any earlier non-relevant match groups (ie, the other regex + // match groups that make up the multi-matcher) + match.splice(0, i); + + return Object.assign(match, matchData); + } + } + + /* + Created to solve the key deficiently with MultiRegex - there is no way to + test for multiple matches at a single location. Why would we need to do + that? In the future a more dynamic engine will allow certain matches to be + ignored. An example: if we matched say the 3rd regex in a large group but + decided to ignore it - we'd need to started testing again at the 4th + regex... but MultiRegex itself gives us no real way to do that. + + So what this class creates MultiRegexs on the fly for whatever search + position they are needed. + + NOTE: These additional MultiRegex objects are created dynamically. For most + grammars most of the time we will never actually need anything more than the + first MultiRegex - so this shouldn't have too much overhead. + + Say this is our search group, and we match regex3, but wish to ignore it. + + regex1 | regex2 | regex3 | regex4 | regex5 ' ie, startAt = 0 + + What we need is a new MultiRegex that only includes the remaining + possibilities: + + regex4 | regex5 ' ie, startAt = 3 + + This class wraps all that complexity up in a simple API... `startAt` decides + where in the array of expressions to start doing the matching. It + auto-increments, so if a match is found at position 2, then startAt will be + set to 3. If the end is reached startAt will return to 0. + + MOST of the time the parser will be setting startAt manually to 0. + */ + class ResumableMultiRegex { + constructor() { + // @ts-ignore + this.rules = []; + // @ts-ignore + this.multiRegexes = []; + this.count = 0; + + this.lastIndex = 0; + this.regexIndex = 0; + } + + // @ts-ignore + getMatcher(index) { + if (this.multiRegexes[index]) return this.multiRegexes[index]; + + const matcher = new MultiRegex(); + this.rules.slice(index).forEach(([re, opts]) => matcher.addRule(re, opts)); + matcher.compile(); + this.multiRegexes[index] = matcher; + return matcher; + } + + resumingScanAtSamePosition() { + return this.regexIndex !== 0; + } + + considerAll() { + this.regexIndex = 0; + } + + // @ts-ignore + addRule(re, opts) { + this.rules.push([re, opts]); + if (opts.type === "begin") this.count++; + } + + /** @param {string} s */ + exec(s) { + const m = this.getMatcher(this.regexIndex); + m.lastIndex = this.lastIndex; + let result = m.exec(s); + + // The following is because we have no easy way to say "resume scanning at the + // existing position but also skip the current rule ONLY". What happens is + // all prior rules are also skipped which can result in matching the wrong + // thing. Example of matching "booger": + + // our matcher is [string, "booger", number] + // + // ....booger.... + + // if "booger" is ignored then we'd really need a regex to scan from the + // SAME position for only: [string, number] but ignoring "booger" (if it + // was the first match), a simple resume would scan ahead who knows how + // far looking only for "number", ignoring potential string matches (or + // future "booger" matches that might be valid.) + + // So what we do: We execute two matchers, one resuming at the same + // position, but the second full matcher starting at the position after: + + // /--- resume first regex match here (for [number]) + // |/---- full match here for [string, "booger", number] + // vv + // ....booger.... + + // Which ever results in a match first is then used. So this 3-4 step + // process essentially allows us to say "match at this position, excluding + // a prior rule that was ignored". + // + // 1. Match "booger" first, ignore. Also proves that [string] does non match. + // 2. Resume matching for [number] + // 3. Match at index + 1 for [string, "booger", number] + // 4. If #2 and #3 result in matches, which came first? + if (this.resumingScanAtSamePosition()) { + if (result && result.index === this.lastIndex); else { // use the second matcher result + const m2 = this.getMatcher(0); + m2.lastIndex = this.lastIndex + 1; + result = m2.exec(s); + } + } + + if (result) { + this.regexIndex += result.position + 1; + if (this.regexIndex === this.count) { + // wrap-around to considering all matches again + this.considerAll(); + } + } + + return result; + } + } + + /** + * Given a mode, builds a huge ResumableMultiRegex that can be used to walk + * the content and find matches. + * + * @param {CompiledMode} mode + * @returns {ResumableMultiRegex} + */ + function buildModeRegex(mode) { + const mm = new ResumableMultiRegex(); + + mode.contains.forEach(term => mm.addRule(term.begin, { rule: term, type: "begin" })); + + if (mode.terminatorEnd) { + mm.addRule(mode.terminatorEnd, { type: "end" }); + } + if (mode.illegal) { + mm.addRule(mode.illegal, { type: "illegal" }); + } + + return mm; + } + + /** skip vs abort vs ignore + * + * @skip - The mode is still entered and exited normally (and contains rules apply), + * but all content is held and added to the parent buffer rather than being + * output when the mode ends. Mostly used with `sublanguage` to build up + * a single large buffer than can be parsed by sublanguage. + * + * - The mode begin ands ends normally. + * - Content matched is added to the parent mode buffer. + * - The parser cursor is moved forward normally. + * + * @abort - A hack placeholder until we have ignore. Aborts the mode (as if it + * never matched) but DOES NOT continue to match subsequent `contains` + * modes. Abort is bad/suboptimal because it can result in modes + * farther down not getting applied because an earlier rule eats the + * content but then aborts. + * + * - The mode does not begin. + * - Content matched by `begin` is added to the mode buffer. + * - The parser cursor is moved forward accordingly. + * + * @ignore - Ignores the mode (as if it never matched) and continues to match any + * subsequent `contains` modes. Ignore isn't technically possible with + * the current parser implementation. + * + * - The mode does not begin. + * - Content matched by `begin` is ignored. + * - The parser cursor is not moved forward. + */ + + /** + * Compiles an individual mode + * + * This can raise an error if the mode contains certain detectable known logic + * issues. + * @param {Mode} mode + * @param {CompiledMode | null} [parent] + * @returns {CompiledMode | never} + */ + function compileMode(mode, parent) { + const cmode = /** @type CompiledMode */ (mode); + if (mode.isCompiled) return cmode; + + [ + scopeClassName, + // do this early so compiler extensions generally don't have to worry about + // the distinction between match/begin + compileMatch, + MultiClass, + beforeMatchExt + ].forEach(ext => ext(mode, parent)); + + language.compilerExtensions.forEach(ext => ext(mode, parent)); + + // __beforeBegin is considered private API, internal use only + mode.__beforeBegin = null; + + [ + beginKeywords, + // do this later so compiler extensions that come earlier have access to the + // raw array if they wanted to perhaps manipulate it, etc. + compileIllegal, + // default to 1 relevance if not specified + compileRelevance + ].forEach(ext => ext(mode, parent)); + + mode.isCompiled = true; + + let keywordPattern = null; + if (typeof mode.keywords === "object" && mode.keywords.$pattern) { + // we need a copy because keywords might be compiled multiple times + // so we can't go deleting $pattern from the original on the first + // pass + mode.keywords = Object.assign({}, mode.keywords); + keywordPattern = mode.keywords.$pattern; + delete mode.keywords.$pattern; + } + keywordPattern = keywordPattern || /\w+/; + + if (mode.keywords) { + mode.keywords = compileKeywords(mode.keywords, language.case_insensitive); + } + + cmode.keywordPatternRe = langRe(keywordPattern, true); + + if (parent) { + if (!mode.begin) mode.begin = /\B|\b/; + cmode.beginRe = langRe(mode.begin); + if (!mode.end && !mode.endsWithParent) mode.end = /\B|\b/; + if (mode.end) cmode.endRe = langRe(mode.end); + cmode.terminatorEnd = source(mode.end) || ''; + if (mode.endsWithParent && parent.terminatorEnd) { + cmode.terminatorEnd += (mode.end ? '|' : '') + parent.terminatorEnd; + } + } + if (mode.illegal) cmode.illegalRe = langRe(/** @type {RegExp | string} */(mode.illegal)); + if (!mode.contains) mode.contains = []; + + mode.contains = [].concat(...mode.contains.map(function (c) { + return expandOrCloneMode(c === 'self' ? mode : c); + })); + mode.contains.forEach(function (c) { compileMode(/** @type Mode */(c), cmode); }); + + if (mode.starts) { + compileMode(mode.starts, parent); + } + + cmode.matcher = buildModeRegex(cmode); + return cmode; + } + + if (!language.compilerExtensions) language.compilerExtensions = []; + + // self is not valid at the top-level + if (language.contains && language.contains.includes('self')) { + throw new Error("ERR: contains `self` is not supported at the top-level of a language. See documentation."); + } + + // we need a null object, which inherit will guarantee + language.classNameAliases = inherit$1(language.classNameAliases || {}); + + return compileMode(/** @type Mode */(language)); + } + + /** + * Determines if a mode has a dependency on it's parent or not + * + * If a mode does have a parent dependency then often we need to clone it if + * it's used in multiple places so that each copy points to the correct parent, + * where-as modes without a parent can often safely be re-used at the bottom of + * a mode chain. + * + * @param {Mode | null} mode + * @returns {boolean} - is there a dependency on the parent? + * */ + function dependencyOnParent(mode) { + if (!mode) return false; + + return mode.endsWithParent || dependencyOnParent(mode.starts); + } + + /** + * Expands a mode or clones it if necessary + * + * This is necessary for modes with parental dependenceis (see notes on + * `dependencyOnParent`) and for nodes that have `variants` - which must then be + * exploded into their own individual modes at compile time. + * + * @param {Mode} mode + * @returns {Mode | Mode[]} + * */ + function expandOrCloneMode(mode) { + if (mode.variants && !mode.cachedVariants) { + mode.cachedVariants = mode.variants.map(function (variant) { + return inherit$1(mode, { variants: null }, variant); + }); + } + + // EXPAND + // if we have variants then essentially "replace" the mode with the variants + // this happens in compileMode, where this function is called from + if (mode.cachedVariants) { + return mode.cachedVariants; + } + + // CLONE + // if we have dependencies on parents then we need a unique + // instance of ourselves, so we can be reused with many + // different parents without issue + if (dependencyOnParent(mode)) { + return inherit$1(mode, { starts: mode.starts ? inherit$1(mode.starts) : null }); + } + + if (Object.isFrozen(mode)) { + return inherit$1(mode); + } + + // no special dependency issues, just return ourselves + return mode; + } + + var version = "11.0.0-beta1"; + + /* + Syntax highlighting with language autodetection. + https://highlightjs.org/ + */ + + /** + @typedef {import('highlight.js').Mode} Mode + @typedef {import('highlight.js').CompiledMode} CompiledMode + @typedef {import('highlight.js').Language} Language + @typedef {import('highlight.js').HLJSApi} HLJSApi + @typedef {import('highlight.js').HLJSPlugin} HLJSPlugin + @typedef {import('highlight.js').PluginEvent} PluginEvent + @typedef {import('highlight.js').HLJSOptions} HLJSOptions + @typedef {import('highlight.js').LanguageFn} LanguageFn + @typedef {import('highlight.js').HighlightedHTMLElement} HighlightedHTMLElement + @typedef {import('highlight.js').BeforeHighlightContext} BeforeHighlightContext + @typedef {import('highlight.js/private').MatchType} MatchType + @typedef {import('highlight.js/private').KeywordData} KeywordData + @typedef {import('highlight.js/private').EnhancedMatch} EnhancedMatch + @typedef {import('highlight.js/private').AnnotatedError} AnnotatedError + @typedef {import('highlight.js').AutoHighlightResult} AutoHighlightResult + @typedef {import('highlight.js').HighlightOptions} HighlightOptions + @typedef {import('highlight.js').HighlightResult} HighlightResult + */ + + + const escape = escapeHTML; + const inherit = inherit$1; + const NO_MATCH = Symbol("nomatch"); + const MAX_KEYWORD_HITS = 7; + + /** + * @param {any} hljs - object that is extended (legacy) + * @returns {HLJSApi} + */ + const HLJS = function (hljs) { + // Global internal variables used within the highlight.js library. + /** @type {Record} */ + const languages = Object.create(null); + /** @type {Record} */ + const aliases = Object.create(null); + /** @type {HLJSPlugin[]} */ + const plugins = []; + + // safe/production mode - swallows more errors, tries to keep running + // even if a single syntax or parse hits a fatal error + let SAFE_MODE = true; + const LANGUAGE_NOT_FOUND = "Could not find the language '{}', did you forget to load/include a language module?"; + /** @type {Language} */ + const PLAINTEXT_LANGUAGE = { disableAutodetect: true, name: 'Plain text', contains: [] }; + + // Global options used when within external APIs. This is modified when + // calling the `hljs.configure` function. + /** @type HLJSOptions */ + let options = { + ignoreUnescapedHTML: false, + noHighlightRe: /^(no-?highlight)$/i, + languageDetectRe: /\blang(?:uage)?-([\w-]+)\b/i, + classPrefix: 'hljs-', + cssSelector: 'pre code', + languages: null, + // beta configuration options, subject to change, welcome to discuss + // https://github.com/highlightjs/highlight.js/issues/1086 + __emitter: TokenTreeEmitter + }; + + /* Utility functions */ + + /** + * Tests a language name to see if highlighting should be skipped + * @param {string} languageName + */ + function shouldNotHighlight(languageName) { + return options.noHighlightRe.test(languageName); + } + + /** + * @param {HighlightedHTMLElement} block - the HTML element to determine language for + */ + function blockLanguage(block) { + let classes = block.className + ' '; + + classes += block.parentNode ? block.parentNode.className : ''; + + // language-* takes precedence over non-prefixed class names. + const match = options.languageDetectRe.exec(classes); + if (match) { + const language = getLanguage(match[1]); + if (!language) { + warn(LANGUAGE_NOT_FOUND.replace("{}", match[1])); + warn("Falling back to no-highlight mode for this block.", block); + } + return language ? match[1] : 'no-highlight'; + } + + return classes + .split(/\s+/) + .find((_class) => shouldNotHighlight(_class) || getLanguage(_class)); + } + + /** + * Core highlighting function. + * + * OLD API + * highlight(lang, code, ignoreIllegals, continuation) + * + * NEW API + * highlight(code, {lang, ignoreIllegals}) + * + * @param {string} codeOrLanguageName - the language to use for highlighting + * @param {string | HighlightOptions} optionsOrCode - the code to highlight + * @param {boolean} [ignoreIllegals] - whether to ignore illegal matches, default is to bail + * @param {CompiledMode} [continuation] - current continuation mode, if any + * + * @returns {HighlightResult} Result - an object that represents the result + * @property {string} language - the language name + * @property {number} relevance - the relevance score + * @property {string} value - the highlighted HTML code + * @property {string} code - the original raw code + * @property {CompiledMode} top - top of the current mode stack + * @property {boolean} illegal - indicates whether any illegal matches were found + */ + function highlight(codeOrLanguageName, optionsOrCode, ignoreIllegals, continuation) { + let code = ""; + let languageName = ""; + if (typeof optionsOrCode === "object") { + code = codeOrLanguageName; + ignoreIllegals = optionsOrCode.ignoreIllegals; + languageName = optionsOrCode.language; + // continuation not supported at all via the new API + // eslint-disable-next-line no-undefined + continuation = undefined; + } else { + // old API + deprecated("10.7.0", "highlight(lang, code, ...args) has been deprecated."); + deprecated("10.7.0", "Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"); + languageName = codeOrLanguageName; + code = optionsOrCode; + } + + // https://github.com/highlightjs/highlight.js/issues/3149 + // eslint-disable-next-line no-undefined + if (ignoreIllegals === undefined) { ignoreIllegals = true; } + + /** @type {BeforeHighlightContext} */ + const context = { + code, + language: languageName + }; + // the plugin can change the desired language or the code to be highlighted + // just be changing the object it was passed + fire("before:highlight", context); + + // a before plugin can usurp the result completely by providing it's own + // in which case we don't even need to call highlight + const result = context.result + ? context.result + : _highlight(context.language, context.code, ignoreIllegals, continuation); + + result.code = context.code; + // the plugin can change anything in result to suite it + fire("after:highlight", result); + + return result; + } + + /** + * private highlight that's used internally and does not fire callbacks + * + * @param {string} languageName - the language to use for highlighting + * @param {string} codeToHighlight - the code to highlight + * @param {boolean?} [ignoreIllegals] - whether to ignore illegal matches, default is to bail + * @param {CompiledMode?} [continuation] - current continuation mode, if any + * @returns {HighlightResult} - result of the highlight operation + */ + function _highlight(languageName, codeToHighlight, ignoreIllegals, continuation) { + const keywordHits = Object.create(null); + + /** + * Return keyword data if a match is a keyword + * @param {CompiledMode} mode - current mode + * @param {string} matchText - the textual match + * @returns {KeywordData | false} + */ + function keywordData(mode, matchText) { + return mode.keywords[matchText]; + } + + function processKeywords() { + if (!top.keywords) { + emitter.addText(modeBuffer); + return; + } + + let lastIndex = 0; + top.keywordPatternRe.lastIndex = 0; + let match = top.keywordPatternRe.exec(modeBuffer); + let buf = ""; + + while (match) { + buf += modeBuffer.substring(lastIndex, match.index); + const word = language.case_insensitive ? match[0].toLowerCase() : match[0]; + const data = keywordData(top, word); + if (data) { + const [kind, keywordRelevance] = data; + emitter.addText(buf); + buf = ""; + + keywordHits[word] = (keywordHits[word] || 0) + 1; + if (keywordHits[word] <= MAX_KEYWORD_HITS) relevance += keywordRelevance; + if (kind.startsWith("_")) { + // _ implied for relevance only, do not highlight + // by applying a class name + buf += match[0]; + } else { + const cssClass = language.classNameAliases[kind] || kind; + emitter.addKeyword(match[0], cssClass); + } + } else { + buf += match[0]; + } + lastIndex = top.keywordPatternRe.lastIndex; + match = top.keywordPatternRe.exec(modeBuffer); + } + buf += modeBuffer.substr(lastIndex); + emitter.addText(buf); + } + + function processSubLanguage() { + if (modeBuffer === "") return; + /** @type HighlightResult */ + let result = null; + + if (typeof top.subLanguage === 'string') { + if (!languages[top.subLanguage]) { + emitter.addText(modeBuffer); + return; + } + result = _highlight(top.subLanguage, modeBuffer, true, continuations[top.subLanguage]); + continuations[top.subLanguage] = /** @type {CompiledMode} */ (result._top); + } else { + result = highlightAuto(modeBuffer, top.subLanguage.length ? top.subLanguage : null); + } + + // Counting embedded language score towards the host language may be disabled + // with zeroing the containing mode relevance. Use case in point is Markdown that + // allows XML everywhere and makes every XML snippet to have a much larger Markdown + // score. + if (top.relevance > 0) { + relevance += result.relevance; + } + emitter.addSublanguage(result._emitter, result.language); + } + + function processBuffer() { + if (top.subLanguage != null) { + processSubLanguage(); + } else { + processKeywords(); + } + modeBuffer = ''; + } + + /** + * @param {CompiledMode} mode + * @param {RegExpMatchArray} match + */ + function emitMultiClass(scope, match) { + let i = 1; + // eslint-disable-next-line no-undefined + while (match[i] !== undefined) { + if (!scope._emit[i]) { i++; continue; } + const klass = language.classNameAliases[scope[i]] || scope[i]; + const text = match[i]; + if (klass) { + emitter.addKeyword(text, klass); + } else { + modeBuffer = text; + processKeywords(); + modeBuffer = ""; + } + i++; + } + } + + /** + * @param {CompiledMode} mode - new mode to start + * @param {RegExpMatchArray} match + */ + function startNewMode(mode, match) { + if (mode.scope && typeof mode.scope === "string") { + emitter.openNode(language.classNameAliases[mode.scope] || mode.scope); + } + if (mode.beginScope) { + // beginScope just wraps the begin match itself in a scope + if (mode.beginScope._wrap) { + emitter.addKeyword(modeBuffer, language.classNameAliases[mode.beginScope._wrap] || mode.beginScope._wrap); + modeBuffer = ""; + } else if (mode.beginScope._multi) { + // at this point modeBuffer should just be the match + emitMultiClass(mode.beginScope, match); + modeBuffer = ""; + } + } + + top = Object.create(mode, { parent: { value: top } }); + return top; + } + + /** + * @param {CompiledMode } mode - the mode to potentially end + * @param {RegExpMatchArray} match - the latest match + * @param {string} matchPlusRemainder - match plus remainder of content + * @returns {CompiledMode | void} - the next mode, or if void continue on in current mode + */ + function endOfMode(mode, match, matchPlusRemainder) { + let matched = startsWith(mode.endRe, matchPlusRemainder); + + if (matched) { + if (mode["on:end"]) { + const resp = new Response(mode); + mode["on:end"](match, resp); + if (resp.isMatchIgnored) matched = false; + } + + if (matched) { + while (mode.endsParent && mode.parent) { + mode = mode.parent; + } + return mode; + } + } + // even if on:end fires an `ignore` it's still possible + // that we might trigger the end node because of a parent mode + if (mode.endsWithParent) { + return endOfMode(mode.parent, match, matchPlusRemainder); + } + } + + /** + * Handle matching but then ignoring a sequence of text + * + * @param {string} lexeme - string containing full match text + */ + function doIgnore(lexeme) { + if (top.matcher.regexIndex === 0) { + // no more regexes to potentially match here, so we move the cursor forward one + // space + modeBuffer += lexeme[0]; + return 1; + } else { + // no need to move the cursor, we still have additional regexes to try and + // match at this very spot + resumeScanAtSamePosition = true; + return 0; + } + } + + /** + * Handle the start of a new potential mode match + * + * @param {EnhancedMatch} match - the current match + * @returns {number} how far to advance the parse cursor + */ + function doBeginMatch(match) { + const lexeme = match[0]; + const newMode = match.rule; + + const resp = new Response(newMode); + // first internal before callbacks, then the public ones + const beforeCallbacks = [newMode.__beforeBegin, newMode["on:begin"]]; + for (const cb of beforeCallbacks) { + if (!cb) continue; + cb(match, resp); + if (resp.isMatchIgnored) return doIgnore(lexeme); + } + + if (newMode.skip) { + modeBuffer += lexeme; + } else { + if (newMode.excludeBegin) { + modeBuffer += lexeme; + } + processBuffer(); + if (!newMode.returnBegin && !newMode.excludeBegin) { + modeBuffer = lexeme; + } + } + startNewMode(newMode, match); + return newMode.returnBegin ? 0 : lexeme.length; + } + + /** + * Handle the potential end of mode + * + * @param {RegExpMatchArray} match - the current match + */ + function doEndMatch(match) { + const lexeme = match[0]; + const matchPlusRemainder = codeToHighlight.substr(match.index); + + const endMode = endOfMode(top, match, matchPlusRemainder); + if (!endMode) { return NO_MATCH; } + + const origin = top; + if (top.endScope && top.endScope._wrap) { + processBuffer(); + emitter.addKeyword(lexeme, top.endScope._wrap); + } else if (top.endScope && top.endScope._multi) { + processBuffer(); + emitMultiClass(top.endScope, match); + } else if (origin.skip) { + modeBuffer += lexeme; + } else { + if (!(origin.returnEnd || origin.excludeEnd)) { + modeBuffer += lexeme; + } + processBuffer(); + if (origin.excludeEnd) { + modeBuffer = lexeme; + } + } + do { + if (top.scope && !top.isMultiClass) { + emitter.closeNode(); + } + if (!top.skip && !top.subLanguage) { + relevance += top.relevance; + } + top = top.parent; + } while (top !== endMode.parent); + if (endMode.starts) { + startNewMode(endMode.starts, match); + } + return origin.returnEnd ? 0 : lexeme.length; + } + + function processContinuations() { + const list = []; + for (let current = top; current !== language; current = current.parent) { + if (current.scope) { + list.unshift(current.scope); + } + } + list.forEach(item => emitter.openNode(item)); + } + + /** @type {{type?: MatchType, index?: number, rule?: Mode}}} */ + let lastMatch = {}; + + /** + * Process an individual match + * + * @param {string} textBeforeMatch - text preceding the match (since the last match) + * @param {EnhancedMatch} [match] - the match itself + */ + function processLexeme(textBeforeMatch, match) { + const lexeme = match && match[0]; + + // add non-matched text to the current mode buffer + modeBuffer += textBeforeMatch; + + if (lexeme == null) { + processBuffer(); + return 0; + } + + // we've found a 0 width match and we're stuck, so we need to advance + // this happens when we have badly behaved rules that have optional matchers to the degree that + // sometimes they can end up matching nothing at all + // Ref: https://github.com/highlightjs/highlight.js/issues/2140 + if (lastMatch.type === "begin" && match.type === "end" && lastMatch.index === match.index && lexeme === "") { + // spit the "skipped" character that our regex choked on back into the output sequence + modeBuffer += codeToHighlight.slice(match.index, match.index + 1); + if (!SAFE_MODE) { + /** @type {AnnotatedError} */ + const err = new Error(`0 width match regex (${languageName})`); + err.languageName = languageName; + err.badRule = lastMatch.rule; + throw err; + } + return 1; + } + lastMatch = match; + + if (match.type === "begin") { + return doBeginMatch(match); + } else if (match.type === "illegal" && !ignoreIllegals) { + // illegal match, we do not continue processing + /** @type {AnnotatedError} */ + const err = new Error('Illegal lexeme "' + lexeme + '" for mode "' + (top.scope || '') + '"'); + err.mode = top; + throw err; + } else if (match.type === "end") { + const processed = doEndMatch(match); + if (processed !== NO_MATCH) { + return processed; + } + } + + // edge case for when illegal matches $ (end of line) which is technically + // a 0 width match but not a begin/end match so it's not caught by the + // first handler (when ignoreIllegals is true) + if (match.type === "illegal" && lexeme === "") { + // advance so we aren't stuck in an infinite loop + return 1; + } + + // infinite loops are BAD, this is a last ditch catch all. if we have a + // decent number of iterations yet our index (cursor position in our + // parsing) still 3x behind our index then something is very wrong + // so we bail + if (iterations > 100000 && iterations > match.index * 3) { + const err = new Error('potential infinite loop, way more iterations than matches'); + throw err; + } + + /* + Why might be find ourselves here? An potential end match that was + triggered but could not be completed. IE, `doEndMatch` returned NO_MATCH. + (this could be because a callback requests the match be ignored, etc) + + This causes no real harm other than stopping a few times too many. + */ + + modeBuffer += lexeme; + return lexeme.length; + } + + const language = getLanguage(languageName); + if (!language) { + error(LANGUAGE_NOT_FOUND.replace("{}", languageName)); + throw new Error('Unknown language: "' + languageName + '"'); + } + + const md = compileLanguage(language); + let result = ''; + /** @type {CompiledMode} */ + let top = continuation || md; + /** @type Record */ + const continuations = {}; // keep continuations for sub-languages + const emitter = new options.__emitter(options); + processContinuations(); + let modeBuffer = ''; + let relevance = 0; + let index = 0; + let iterations = 0; + let resumeScanAtSamePosition = false; + + try { + top.matcher.considerAll(); + + for (; ;) { + iterations++; + if (resumeScanAtSamePosition) { + // only regexes not matched previously will now be + // considered for a potential match + resumeScanAtSamePosition = false; + } else { + top.matcher.considerAll(); + } + top.matcher.lastIndex = index; + + const match = top.matcher.exec(codeToHighlight); + // console.log("match", match[0], match.rule && match.rule.begin) + + if (!match) break; + + const beforeMatch = codeToHighlight.substring(index, match.index); + const processedCount = processLexeme(beforeMatch, match); + index = match.index + processedCount; + } + processLexeme(codeToHighlight.substr(index)); + emitter.closeAllNodes(); + emitter.finalize(); + result = emitter.toHTML(); + + return { + language: languageName, + value: result, + relevance: relevance, + illegal: false, + _emitter: emitter, + _top: top + }; + } catch (err) { + if (err.message && err.message.includes('Illegal')) { + return { + language: languageName, + value: escape(codeToHighlight), + illegal: true, + relevance: 0, + _illegalBy: { + message: err.message, + index: index, + context: codeToHighlight.slice(index - 100, index + 100), + mode: err.mode, + resultSoFar: result + }, + _emitter: emitter + }; + } else if (SAFE_MODE) { + return { + language: languageName, + value: escape(codeToHighlight), + illegal: false, + relevance: 0, + errorRaised: err, + _emitter: emitter, + _top: top + }; + } else { + throw err; + } + } + } + + /** + * returns a valid highlight result, without actually doing any actual work, + * auto highlight starts with this and it's possible for small snippets that + * auto-detection may not find a better match + * @param {string} code + * @returns {HighlightResult} + */ + function justTextHighlightResult(code) { + const result = { + value: escape(code), + illegal: false, + relevance: 0, + _top: PLAINTEXT_LANGUAGE, + _emitter: new options.__emitter(options) + }; + result._emitter.addText(code); + return result; + } + + /** + Highlighting with language detection. Accepts a string with the code to + highlight. Returns an object with the following properties: + + - language (detected language) + - relevance (int) + - value (an HTML string with highlighting markup) + - secondBest (object with the same structure for second-best heuristically + detected language, may be absent) + + @param {string} code + @param {Array} [languageSubset] + @returns {AutoHighlightResult} + */ + function highlightAuto(code, languageSubset) { + languageSubset = languageSubset || options.languages || Object.keys(languages); + const plaintext = justTextHighlightResult(code); + + const results = languageSubset.filter(getLanguage).filter(autoDetection).map(name => + _highlight(name, code, false) + ); + results.unshift(plaintext); // plaintext is always an option + + const sorted = results.sort((a, b) => { + // sort base on relevance + if (a.relevance !== b.relevance) return b.relevance - a.relevance; + + // always award the tie to the base language + // ie if C++ and Arduino are tied, it's more likely to be C++ + if (a.language && b.language) { + if (getLanguage(a.language).supersetOf === b.language) { + return 1; + } else if (getLanguage(b.language).supersetOf === a.language) { + return -1; + } + } + + // otherwise say they are equal, which has the effect of sorting on + // relevance while preserving the original ordering - which is how ties + // have historically been settled, ie the language that comes first always + // wins in the case of a tie + return 0; + }); + + const [best, secondBest] = sorted; + + /** @type {AutoHighlightResult} */ + const result = best; + result.secondBest = secondBest; + + return result; + } + + /** + * Builds new class name for block given the language name + * + * @param {HTMLElement} element + * @param {string} [currentLang] + * @param {string} [resultLang] + */ + function updateClassName(element, currentLang, resultLang) { + const language = (currentLang && aliases[currentLang]) || resultLang; + + element.classList.add("hljs"); + element.classList.add(`language-${language}`); + } + + /** + * Applies highlighting to a DOM node containing code. + * + * @param {HighlightedHTMLElement} element - the HTML element to highlight + */ + function highlightElement(element) { + /** @type HTMLElement */ + let node = null; + const language = blockLanguage(element); + + if (shouldNotHighlight(language)) return; + + fire("before:highlightElement", + { el: element, language: language }); + + // we should be all text, no child nodes + if (!options.ignoreUnescapedHTML && element.children.length > 0) { + console.warn("One of your code blocks includes unescaped HTML. This is a potentially serious security risk."); + console.warn("https://github.com/highlightjs/highlight.js/issues/2886"); + console.warn(element); + } + + node = element; + const text = node.textContent; + const result = language ? highlight(text, { language, ignoreIllegals: true }) : highlightAuto(text); + + fire("after:highlightElement", { el: element, result, text }); + + element.innerHTML = result.value; + updateClassName(element, language, result.language); + element.result = { + language: result.language, + // TODO: remove with version 11.0 + re: result.relevance, + relevance: result.relevance + }; + if (result.secondBest) { + element.secondBest = { + language: result.secondBest.language, + relevance: result.secondBest.relevance + }; + } + } + + /** + * Updates highlight.js global options with the passed options + * + * @param {Partial} userOptions + */ + function configure(userOptions) { + options = inherit(options, userOptions); + } + + // TODO: remove v12, deprecated + const initHighlighting = () => { + highlightAll(); + deprecated("10.6.0", "initHighlighting() deprecated. Use highlightAll() now."); + }; + + // TODO: remove v12, deprecated + function initHighlightingOnLoad() { + highlightAll(); + deprecated("10.6.0", "initHighlightingOnLoad() deprecated. Use highlightAll() now."); + } + + let wantsHighlight = false; + + /** + * auto-highlights all pre>code elements on the page + */ + function highlightAll() { + // if we are called too early in the loading process + if (document.readyState === "loading") { + wantsHighlight = true; + return; + } + + const blocks = document.querySelectorAll(options.cssSelector); + blocks.forEach(highlightElement); + } + + function boot() { + // if a highlight was requested before DOM was loaded, do now + if (wantsHighlight) highlightAll(); + } + + // make sure we are in the browser environment + if (typeof window !== 'undefined' && window.addEventListener) { + window.addEventListener('DOMContentLoaded', boot, false); + } + + /** + * Register a language grammar module + * + * @param {string} languageName + * @param {LanguageFn} languageDefinition + */ + function registerLanguage(languageName, languageDefinition) { + let lang = null; + try { + lang = languageDefinition(hljs); + } catch (error$1) { + error("Language definition for '{}' could not be registered.".replace("{}", languageName)); + // hard or soft error + if (!SAFE_MODE) { throw error$1; } else { error(error$1); } + // languages that have serious errors are replaced with essentially a + // "plaintext" stand-in so that the code blocks will still get normal + // css classes applied to them - and one bad language won't break the + // entire highlighter + lang = PLAINTEXT_LANGUAGE; + } + // give it a temporary name if it doesn't have one in the meta-data + if (!lang.name) lang.name = languageName; + languages[languageName] = lang; + lang.rawDefinition = languageDefinition.bind(null, hljs); + + if (lang.aliases) { + registerAliases(lang.aliases, { languageName }); + } + } + + /** + * Remove a language grammar module + * + * @param {string} languageName + */ + function unregisterLanguage(languageName) { + delete languages[languageName]; + for (const alias of Object.keys(aliases)) { + if (aliases[alias] === languageName) { + delete aliases[alias]; + } + } + } + + /** + * @returns {string[]} List of language internal names + */ + function listLanguages() { + return Object.keys(languages); + } + + /** + * @param {string} name - name of the language to retrieve + * @returns {Language | undefined} + */ + function getLanguage(name) { + name = (name || '').toLowerCase(); + return languages[name] || languages[aliases[name]]; + } + + /** + * + * @param {string|string[]} aliasList - single alias or list of aliases + * @param {{languageName: string}} opts + */ + function registerAliases(aliasList, { languageName }) { + if (typeof aliasList === 'string') { + aliasList = [aliasList]; + } + aliasList.forEach(alias => { aliases[alias.toLowerCase()] = languageName; }); + } + + /** + * Determines if a given language has auto-detection enabled + * @param {string} name - name of the language + */ + function autoDetection(name) { + const lang = getLanguage(name); + return lang && !lang.disableAutodetect; + } + + /** + * Upgrades the old highlightBlock plugins to the new + * highlightElement API + * @param {HLJSPlugin} plugin + */ + function upgradePluginAPI(plugin) { + // TODO: remove with v12 + if (plugin["before:highlightBlock"] && !plugin["before:highlightElement"]) { + plugin["before:highlightElement"] = (data) => { + plugin["before:highlightBlock"]( + Object.assign({ block: data.el }, data) + ); + }; + } + if (plugin["after:highlightBlock"] && !plugin["after:highlightElement"]) { + plugin["after:highlightElement"] = (data) => { + plugin["after:highlightBlock"]( + Object.assign({ block: data.el }, data) + ); + }; + } + } + + /** + * @param {HLJSPlugin} plugin + */ + function addPlugin(plugin) { + upgradePluginAPI(plugin); + plugins.push(plugin); + } + + /** + * + * @param {PluginEvent} event + * @param {any} args + */ + function fire(event, args) { + const cb = event; + plugins.forEach(function (plugin) { + if (plugin[cb]) { + plugin[cb](args); + } + }); + } + + /** + * + * @param {HighlightedHTMLElement} el + */ + function deprecateHighlightBlock(el) { + deprecated("10.7.0", "highlightBlock will be removed entirely in v12.0"); + deprecated("10.7.0", "Please use highlightElement now."); + + return highlightElement(el); + } + + /* Interface definition */ + Object.assign(hljs, { + highlight, + highlightAuto, + highlightAll, + highlightElement, + // TODO: Remove with v12 API + highlightBlock: deprecateHighlightBlock, + configure, + initHighlighting, + initHighlightingOnLoad, + registerLanguage, + unregisterLanguage, + listLanguages, + getLanguage, + registerAliases, + autoDetection, + inherit, + addPlugin + }); + + hljs.debugMode = function () { SAFE_MODE = false; }; + hljs.safeMode = function () { SAFE_MODE = true; }; + hljs.versionString = version; + + for (const key in MODES$1) { + // @ts-ignore + if (typeof MODES$1[key] === "object") { + // @ts-ignore + deepFreeze$1(MODES$1[key]); + } + } + + // merge all the modes/regexes into our main object + Object.assign(hljs, MODES$1); + + return hljs; + }; + + // export an "instance" of the highlighter + var HighlightJS = HLJS({}); + + /* + Language: Bash + Author: vah + Contributrors: Benjamin Pannell + Website: https://www.gnu.org/software/bash/ + Category: common + */ + + /** @type LanguageFn */ + function bash(hljs) { + const VAR = {}; + const BRACED_VAR = { + begin: /\$\{/, + end: /\}/, + contains: [ + "self", + { + begin: /:-/, + contains: [VAR] + } // default values + ] + }; + Object.assign(VAR, { + className: 'variable', + variants: [ + { + begin: concat(/\$[\w\d#@][\w\d_]*/, + // negative look-ahead tries to avoid matching patterns that are not + // Perl at all like $ident$, @ident@, etc. + `(?![\\w\\d])(?![$])`) + }, + BRACED_VAR + ] + }); + + const SUBST = { + className: 'subst', + begin: /\$\(/, end: /\)/, + contains: [hljs.BACKSLASH_ESCAPE] + }; + const HERE_DOC = { + begin: /<<-?\s*(?=\w+)/, + starts: { + contains: [ + hljs.END_SAME_AS_BEGIN({ + begin: /(\w+)/, + end: /(\w+)/, + className: 'string' + }) + ] + } + }; + const QUOTE_STRING = { + className: 'string', + begin: /"/, end: /"/, + contains: [ + hljs.BACKSLASH_ESCAPE, + VAR, + SUBST + ] + }; + SUBST.contains.push(QUOTE_STRING); + const ESCAPED_QUOTE = { + className: '', + begin: /\\"/ + + }; + const APOS_STRING = { + className: 'string', + begin: /'/, end: /'/ + }; + const ARITHMETIC = { + begin: /\$\(\(/, + end: /\)\)/, + contains: [ + { begin: /\d+#[0-9a-f]+/, className: "number" }, + hljs.NUMBER_MODE, + VAR + ] + }; + const SH_LIKE_SHELLS = [ + "fish", + "bash", + "zsh", + "sh", + "csh", + "ksh", + "tcsh", + "dash", + "scsh", + ]; + const KNOWN_SHEBANG = hljs.SHEBANG({ + binary: `(${SH_LIKE_SHELLS.join("|")})`, + relevance: 10 + }); + const FUNCTION = { + className: 'function', + begin: /\w[\w\d_]*\s*\(\s*\)\s*\{/, + returnBegin: true, + contains: [hljs.inherit(hljs.TITLE_MODE, { begin: /\w[\w\d_]*/ })], + relevance: 0 + }; + + const KEYWORDS = [ + "if", + "then", + "else", + "elif", + "fi", + "for", + "while", + "in", + "do", + "done", + "case", + "esac", + "function" + ]; + + const LITERALS = [ + "true", + "false" + ]; + + return { + name: 'Bash', + aliases: ['sh'], + keywords: { + $pattern: /\b[a-z._-]+\b/, + keyword: KEYWORDS, + literal: LITERALS, + built_in: + // Shell built-ins + // http://www.gnu.org/software/bash/manual/html_node/Shell-Builtin-Commands.html + 'break cd continue eval exec exit export getopts hash pwd readonly return shift test times ' + + 'trap umask unset ' + + // Bash built-ins + 'alias bind builtin caller command declare echo enable help let local logout mapfile printf ' + + 'read readarray source type typeset ulimit unalias ' + + // Shell modifiers + 'set shopt ' + + // Zsh built-ins + 'autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles ' + + 'compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate ' + + 'fc fg float functions getcap getln history integer jobs kill limit log noglob popd print ' + + 'pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit ' + + 'unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof ' + + 'zpty zregexparse zsocket zstyle ztcp' + }, + contains: [ + KNOWN_SHEBANG, // to catch known shells and boost relevancy + hljs.SHEBANG(), // to catch unknown shells but still highlight the shebang + FUNCTION, + ARITHMETIC, + hljs.HASH_COMMENT_MODE, + HERE_DOC, + QUOTE_STRING, + ESCAPED_QUOTE, + APOS_STRING, + VAR + ] + }; + } + + + const MODES = (hljs) => { + return { + IMPORTANT: { + scope: 'meta', + begin: '!important' + }, + HEXCOLOR: { + scope: 'number', + begin: '#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})' + }, + ATTRIBUTE_SELECTOR_MODE: { + scope: 'selector-attr', + begin: /\[/, + end: /\]/, + illegal: '$', + contains: [ + hljs.APOS_STRING_MODE, + hljs.QUOTE_STRING_MODE + ] + }, + CSS_NUMBER_MODE: { + scope: 'number', + begin: hljs.NUMBER_RE + '(' + + '%|em|ex|ch|rem' + + '|vw|vh|vmin|vmax' + + '|cm|mm|in|pt|pc|px' + + '|deg|grad|rad|turn' + + '|s|ms' + + '|Hz|kHz' + + '|dpi|dpcm|dppx' + + ')?', + relevance: 0 + } + }; + }; + + const TAGS = [ + 'a', + 'abbr', + 'address', + 'article', + 'aside', + 'audio', + 'b', + 'blockquote', + 'body', + 'button', + 'canvas', + 'caption', + 'cite', + 'code', + 'dd', + 'del', + 'details', + 'dfn', + 'div', + 'dl', + 'dt', + 'em', + 'fieldset', + 'figcaption', + 'figure', + 'footer', + 'form', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'header', + 'hgroup', + 'html', + 'i', + 'iframe', + 'img', + 'input', + 'ins', + 'kbd', + 'label', + 'legend', + 'li', + 'main', + 'mark', + 'menu', + 'nav', + 'object', + 'ol', + 'p', + 'q', + 'quote', + 'samp', + 'section', + 'span', + 'strong', + 'summary', + 'sup', + 'table', + 'tbody', + 'td', + 'textarea', + 'tfoot', + 'th', + 'thead', + 'time', + 'tr', + 'ul', + 'var', + 'video' + ]; + + const MEDIA_FEATURES = [ + 'any-hover', + 'any-pointer', + 'aspect-ratio', + 'color', + 'color-gamut', + 'color-index', + 'device-aspect-ratio', + 'device-height', + 'device-width', + 'display-mode', + 'forced-colors', + 'grid', + 'height', + 'hover', + 'inverted-colors', + 'monochrome', + 'orientation', + 'overflow-block', + 'overflow-inline', + 'pointer', + 'prefers-color-scheme', + 'prefers-contrast', + 'prefers-reduced-motion', + 'prefers-reduced-transparency', + 'resolution', + 'scan', + 'scripting', + 'update', + 'width', + // TODO: find a better solution? + 'min-width', + 'max-width', + 'min-height', + 'max-height' + ]; + + // https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes + const PSEUDO_CLASSES = [ + 'active', + 'any-link', + 'blank', + 'checked', + 'current', + 'default', + 'defined', + 'dir', // dir() + 'disabled', + 'drop', + 'empty', + 'enabled', + 'first', + 'first-child', + 'first-of-type', + 'fullscreen', + 'future', + 'focus', + 'focus-visible', + 'focus-within', + 'has', // has() + 'host', // host or host() + 'host-context', // host-context() + 'hover', + 'indeterminate', + 'in-range', + 'invalid', + 'is', // is() + 'lang', // lang() + 'last-child', + 'last-of-type', + 'left', + 'link', + 'local-link', + 'not', // not() + 'nth-child', // nth-child() + 'nth-col', // nth-col() + 'nth-last-child', // nth-last-child() + 'nth-last-col', // nth-last-col() + 'nth-last-of-type', //nth-last-of-type() + 'nth-of-type', //nth-of-type() + 'only-child', + 'only-of-type', + 'optional', + 'out-of-range', + 'past', + 'placeholder-shown', + 'read-only', + 'read-write', + 'required', + 'right', + 'root', + 'scope', + 'target', + 'target-within', + 'user-invalid', + 'valid', + 'visited', + 'where' // where() + ]; + + // https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-elements + const PSEUDO_ELEMENTS = [ + 'after', + 'backdrop', + 'before', + 'cue', + 'cue-region', + 'first-letter', + 'first-line', + 'grammar-error', + 'marker', + 'part', + 'placeholder', + 'selection', + 'slotted', + 'spelling-error' + ]; + + const ATTRIBUTES = [ + 'align-content', + 'align-items', + 'align-self', + 'animation', + 'animation-delay', + 'animation-direction', + 'animation-duration', + 'animation-fill-mode', + 'animation-iteration-count', + 'animation-name', + 'animation-play-state', + 'animation-timing-function', + 'auto', + 'backface-visibility', + 'background', + 'background-attachment', + 'background-clip', + 'background-color', + 'background-image', + 'background-origin', + 'background-position', + 'background-repeat', + 'background-size', + 'border', + 'border-bottom', + 'border-bottom-color', + 'border-bottom-left-radius', + 'border-bottom-right-radius', + 'border-bottom-style', + 'border-bottom-width', + 'border-collapse', + 'border-color', + 'border-image', + 'border-image-outset', + 'border-image-repeat', + 'border-image-slice', + 'border-image-source', + 'border-image-width', + 'border-left', + 'border-left-color', + 'border-left-style', + 'border-left-width', + 'border-radius', + 'border-right', + 'border-right-color', + 'border-right-style', + 'border-right-width', + 'border-spacing', + 'border-style', + 'border-top', + 'border-top-color', + 'border-top-left-radius', + 'border-top-right-radius', + 'border-top-style', + 'border-top-width', + 'border-width', + 'bottom', + 'box-decoration-break', + 'box-shadow', + 'box-sizing', + 'break-after', + 'break-before', + 'break-inside', + 'caption-side', + 'clear', + 'clip', + 'clip-path', + 'color', + 'column-count', + 'column-fill', + 'column-gap', + 'column-rule', + 'column-rule-color', + 'column-rule-style', + 'column-rule-width', + 'column-span', + 'column-width', + 'columns', + 'content', + 'counter-increment', + 'counter-reset', + 'cursor', + 'direction', + 'display', + 'empty-cells', + 'filter', + 'flex', + 'flex-basis', + 'flex-direction', + 'flex-flow', + 'flex-grow', + 'flex-shrink', + 'flex-wrap', + 'float', + 'font', + 'font-display', + 'font-family', + 'font-feature-settings', + 'font-kerning', + 'font-language-override', + 'font-size', + 'font-size-adjust', + 'font-smoothing', + 'font-stretch', + 'font-style', + 'font-variant', + 'font-variant-ligatures', + 'font-variation-settings', + 'font-weight', + 'height', + 'hyphens', + 'icon', + 'image-orientation', + 'image-rendering', + 'image-resolution', + 'ime-mode', + 'inherit', + 'initial', + 'justify-content', + 'left', + 'letter-spacing', + 'line-height', + 'list-style', + 'list-style-image', + 'list-style-position', + 'list-style-type', + 'margin', + 'margin-bottom', + 'margin-left', + 'margin-right', + 'margin-top', + 'marks', + 'mask', + 'max-height', + 'max-width', + 'min-height', + 'min-width', + 'nav-down', + 'nav-index', + 'nav-left', + 'nav-right', + 'nav-up', + 'none', + 'normal', + 'object-fit', + 'object-position', + 'opacity', + 'order', + 'orphans', + 'outline', + 'outline-color', + 'outline-offset', + 'outline-style', + 'outline-width', + 'overflow', + 'overflow-wrap', + 'overflow-x', + 'overflow-y', + 'padding', + 'padding-bottom', + 'padding-left', + 'padding-right', + 'padding-top', + 'page-break-after', + 'page-break-before', + 'page-break-inside', + 'perspective', + 'perspective-origin', + 'pointer-events', + 'position', + 'quotes', + 'resize', + 'right', + 'src', // @font-face + 'tab-size', + 'table-layout', + 'text-align', + 'text-align-last', + 'text-decoration', + 'text-decoration-color', + 'text-decoration-line', + 'text-decoration-style', + 'text-indent', + 'text-overflow', + 'text-rendering', + 'text-shadow', + 'text-transform', + 'text-underline-position', + 'top', + 'transform', + 'transform-origin', + 'transform-style', + 'transition', + 'transition-delay', + 'transition-duration', + 'transition-property', + 'transition-timing-function', + 'unicode-bidi', + 'vertical-align', + 'visibility', + 'white-space', + 'widows', + 'width', + 'word-break', + 'word-spacing', + 'word-wrap', + 'z-index' + // reverse makes sure longer attributes `font-weight` are matched fully + // instead of getting false positives on say `font` + ].reverse(); + + // some grammars use them all as a single group + const PSEUDO_SELECTORS = PSEUDO_CLASSES.concat(PSEUDO_ELEMENTS); + + + // https://docs.oracle.com/javase/specs/jls/se15/html/jls-3.html#jls-3.10 + var decimalDigits = '[0-9](_*[0-9])*'; + var frac = `\\.(${decimalDigits})`; + var hexDigits = '[0-9a-fA-F](_*[0-9a-fA-F])*'; + var NUMERIC = { + className: 'number', + variants: [ + // DecimalFloatingPointLiteral + // including ExponentPart + { + begin: `(\\b(${decimalDigits})((${frac})|\\.)?|(${frac}))` + + `[eE][+-]?(${decimalDigits})[fFdD]?\\b` + }, + // excluding ExponentPart + { begin: `\\b(${decimalDigits})((${frac})[fFdD]?\\b|\\.([fFdD]\\b)?)` }, + { begin: `(${frac})[fFdD]?\\b` }, + { begin: `\\b(${decimalDigits})[fFdD]\\b` }, + + // HexadecimalFloatingPointLiteral + { + begin: `\\b0[xX]((${hexDigits})\\.?|(${hexDigits})?\\.(${hexDigits}))` + + `[pP][+-]?(${decimalDigits})[fFdD]?\\b` + }, + + // DecimalIntegerLiteral + { begin: '\\b(0|[1-9](_*[0-9])*)[lL]?\\b' }, + + // HexIntegerLiteral + { begin: `\\b0[xX](${hexDigits})[lL]?\\b` }, + + // OctalIntegerLiteral + { begin: '\\b0(_*[0-7])*[lL]?\\b' }, + + // BinaryIntegerLiteral + { begin: '\\b0[bB][01](_*[01])*[lL]?\\b' }, + ], + relevance: 0 + }; + + + /** + * Allows recursive regex expressions to a given depth + * + * ie: recurRegex("(abc~~~)", /~~~/g, 2) becomes: + * (abc(abc(abc))) + * + * @param {string} re + * @param {RegExp} substitution (should be a g mode regex) + * @param {number} depth + * @returns {string}`` + */ + function recurRegex(re, substitution, depth) { + if (depth === -1) return ""; + + return re.replace(substitution, _ => { + return recurRegex(re, substitution, depth - 1); + }); + } + + const IDENT_RE = '[A-Za-z$_][0-9A-Za-z$_]*'; + const KEYWORDS = [ + "as", // for exports + "in", + "of", + "if", + "for", + "while", + "finally", + "var", + "new", + "function", + "do", + "return", + "void", + "else", + "break", + "catch", + "instanceof", + "with", + "throw", + "case", + "default", + "try", + "switch", + "continue", + "typeof", + "delete", + "let", + "yield", + "const", + "class", + // JS handles these with a special rule + // "get", + // "set", + "debugger", + "async", + "await", + "static", + "import", + "from", + "export", + "extends" + ]; + const LITERALS = [ + "true", + "false", + "null", + "undefined", + "NaN", + "Infinity" + ]; + + const TYPES = [ + "Intl", + "DataView", + "Number", + "Math", + "Date", + "String", + "RegExp", + "Object", + "Function", + "Boolean", + "Error", + "Symbol", + "Set", + "Map", + "WeakSet", + "WeakMap", + "Proxy", + "Reflect", + "JSON", + "Promise", + "Float64Array", + "Int16Array", + "Int32Array", + "Int8Array", + "Uint16Array", + "Uint32Array", + "Float32Array", + "Array", + "Uint8Array", + "Uint8ClampedArray", + "ArrayBuffer", + "BigInt64Array", + "BigUint64Array", + "BigInt" + ]; + + const ERROR_TYPES = [ + "EvalError", + "InternalError", + "RangeError", + "ReferenceError", + "SyntaxError", + "TypeError", + "URIError" + ]; + + const BUILT_IN_GLOBALS = [ + "setInterval", + "setTimeout", + "clearInterval", + "clearTimeout", + + "require", + "exports", + + "eval", + "isFinite", + "isNaN", + "parseFloat", + "parseInt", + "decodeURI", + "decodeURIComponent", + "encodeURI", + "encodeURIComponent", + "escape", + "unescape" + ]; + + const BUILT_IN_VARIABLES = [ + "arguments", + "this", + "super", + "console", + "window", + "document", + "localStorage", + "module", + "global" // Node.js + ]; + + const BUILT_INS = [].concat( + BUILT_IN_GLOBALS, + TYPES, + ERROR_TYPES + ); + + /* + Language: JavaScript + Description: JavaScript (JS) is a lightweight, interpreted, or just-in-time compiled programming language with first-class functions. + Category: common, scripting, web + Website: https://developer.mozilla.org/en-US/docs/Web/JavaScript + */ + + /** @type LanguageFn */ + function javascript(hljs) { + /** + * Takes a string like " { + const tag = "', + end: '' + }; + const XML_TAG = { + begin: /<[A-Za-z0-9\\._:-]+/, + end: /\/[A-Za-z0-9\\._:-]+>|\/>/, + /** + * @param {RegExpMatchArray} match + * @param {CallbackResponse} response + */ + isTrulyOpeningTag: (match, response) => { + const afterMatchIndex = match[0].length + match.index; + const nextChar = match.input[afterMatchIndex]; + // nested type? + // HTML should not include another raw `<` inside a tag + // But a type might: `>`, etc. + if (nextChar === "<") { + response.ignoreMatch(); + return; + } + // + // This is now either a tag or a type. + if (nextChar === ">") { + // if we cannot find a matching closing tag, then we + // will ignore it + if (!hasClosingTag(match, { after: afterMatchIndex })) { + response.ignoreMatch(); + } + } + } + }; + const KEYWORDS$1 = { + $pattern: IDENT_RE, + keyword: KEYWORDS, + literal: LITERALS, + built_in: BUILT_INS, + "variable.language": BUILT_IN_VARIABLES + }; + + // https://tc39.es/ecma262/#sec-literals-numeric-literals + const decimalDigits = '[0-9](_?[0-9])*'; + const frac = `\\.(${decimalDigits})`; + // DecimalIntegerLiteral, including Annex B NonOctalDecimalIntegerLiteral + // https://tc39.es/ecma262/#sec-additional-syntax-numeric-literals + const decimalInteger = `0|[1-9](_?[0-9])*|0[0-7]*[89][0-9]*`; + const NUMBER = { + className: 'number', + variants: [ + // DecimalLiteral + { + begin: `(\\b(${decimalInteger})((${frac})|\\.)?|(${frac}))` + + `[eE][+-]?(${decimalDigits})\\b` + }, + { begin: `\\b(${decimalInteger})\\b((${frac})\\b|\\.)?|(${frac})\\b` }, + + // DecimalBigIntegerLiteral + { begin: `\\b(0|[1-9](_?[0-9])*)n\\b` }, + + // NonDecimalIntegerLiteral + { begin: "\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*n?\\b" }, + { begin: "\\b0[bB][0-1](_?[0-1])*n?\\b" }, + { begin: "\\b0[oO][0-7](_?[0-7])*n?\\b" }, + + // LegacyOctalIntegerLiteral (does not include underscore separators) + // https://tc39.es/ecma262/#sec-additional-syntax-numeric-literals + { begin: "\\b0[0-7]+n?\\b" }, + ], + relevance: 0 + }; + + const SUBST = { + className: 'subst', + begin: '\\$\\{', + end: '\\}', + keywords: KEYWORDS$1, + contains: [] // defined later + }; + const HTML_TEMPLATE = { + begin: 'html`', + end: '', + starts: { + end: '`', + returnEnd: false, + contains: [ + hljs.BACKSLASH_ESCAPE, + SUBST + ], + subLanguage: 'xml' + } + }; + const CSS_TEMPLATE = { + begin: 'css`', + end: '', + starts: { + end: '`', + returnEnd: false, + contains: [ + hljs.BACKSLASH_ESCAPE, + SUBST + ], + subLanguage: 'css' + } + }; + const TEMPLATE_STRING = { + className: 'string', + begin: '`', + end: '`', + contains: [ + hljs.BACKSLASH_ESCAPE, + SUBST + ] + }; + const JSDOC_COMMENT = hljs.COMMENT( + /\/\*\*(?!\/)/, + '\\*/', + { + relevance: 0, + contains: [ + { + begin: '(?=@[A-Za-z]+)', + relevance: 0, + contains: [ + { + className: 'doctag', + begin: '@[A-Za-z]+' + }, + { + className: 'type', + begin: '\\{', + end: '\\}', + excludeEnd: true, + excludeBegin: true, + relevance: 0 + }, + { + className: 'variable', + begin: IDENT_RE$1 + '(?=\\s*(-)|$)', + endsParent: true, + relevance: 0 + }, + // eat spaces (not newlines) so we can find + // types or variables + { + begin: /(?=[^\n])\s/, + relevance: 0 + } + ] + } + ] + } + ); + const COMMENT = { + className: "comment", + variants: [ + JSDOC_COMMENT, + hljs.C_BLOCK_COMMENT_MODE, + hljs.C_LINE_COMMENT_MODE + ] + }; + const SUBST_INTERNALS = [ + hljs.APOS_STRING_MODE, + hljs.QUOTE_STRING_MODE, + HTML_TEMPLATE, + CSS_TEMPLATE, + TEMPLATE_STRING, + NUMBER, + hljs.REGEXP_MODE + ]; + SUBST.contains = SUBST_INTERNALS + .concat({ + // we need to pair up {} inside our subst to prevent + // it from ending too early by matching another } + begin: /\{/, + end: /\}/, + keywords: KEYWORDS$1, + contains: [ + "self" + ].concat(SUBST_INTERNALS) + }); + const SUBST_AND_COMMENTS = [].concat(COMMENT, SUBST.contains); + const PARAMS_CONTAINS = SUBST_AND_COMMENTS.concat([ + // eat recursive parens in sub expressions + { + begin: /\(/, + end: /\)/, + keywords: KEYWORDS$1, + contains: ["self"].concat(SUBST_AND_COMMENTS) + } + ]); + const PARAMS = { + className: 'params', + begin: /\(/, + end: /\)/, + excludeBegin: true, + excludeEnd: true, + keywords: KEYWORDS$1, + contains: PARAMS_CONTAINS + }; + + // ES6 classes + const CLASS_OR_EXTENDS = { + variants: [ + { + match: [ + /class/, + /\s+/, + IDENT_RE$1 + ], + scope: { + 1: "keyword", + 3: "title.class" + } + }, + { + match: [ + /extends/, + /\s+/, + concat(IDENT_RE$1, "(", concat(/\./, IDENT_RE$1), ")*") + ], + scope: { + 1: "keyword", + 3: "title.class.inherited" + } + } + ] + }; + + const CLASS_REFERENCE = { + relevance: 0, + match: /\b[A-Z][a-z]+([A-Z][a-z]+)*/, + className: "title.class", + keywords: { + _: [ + // se we still get relevance credit for JS library classes + ...TYPES, + ...ERROR_TYPES + ] + } + }; + + const USE_STRICT = { + label: "use_strict", + className: 'meta', + relevance: 10, + begin: /^\s*['"]use (strict|asm)['"]/ + }; + + const FUNCTION_DEFINITION = { + variants: [ + { + match: [ + /function/, + /\s+/, + IDENT_RE$1, + /(?=\s*\()/ + ] + }, + // anonymous function + { + match: [ + /function/, + /\s*(?=\()/ + ] + } + ], + className: { + 1: "keyword", + 3: "title.function" + }, + label: "func.def", + contains: [PARAMS], + illegal: /%/ + }; + + const UPPER_CASE_CONSTANT = { + relevance: 0, + match: /\b[A-Z][A-Z_]+\b/, + className: "variable.constant" + }; + + function noneOf(list) { + return concat("(?!", list.join("|"), ")"); + } + + const FUNCTION_CALL = { + match: concat( + /\b/, + noneOf([ + ...BUILT_IN_GLOBALS, + "super" + ]), + IDENT_RE$1, lookahead(/\(/)), + className: "title.function", + relevance: 0 + }; + + const PROPERTY_ACCESS = { + begin: concat(/\./, lookahead( + concat(IDENT_RE$1, /(?![0-9A-Za-z$_(])/) + )), + end: IDENT_RE$1, + excludeBegin: true, + keywords: "prototype", + className: "property", + relevance: 0 + }; + + const GETTER_OR_SETTER = { + match: [ + /get|set/, + /\s+/, + IDENT_RE$1, + /(?=\()/ + ], + className: { + 1: "keyword", + 3: "title.function" + }, + contains: [ + { // eat to avoid empty params + begin: /\(\)/ + }, + PARAMS + ] + }; + + const FUNC_LEAD_IN_RE = '(\\(' + + '[^()]*(\\(' + + '[^()]*(\\(' + + '[^()]*' + + '\\)[^()]*)*' + + '\\)[^()]*)*' + + '\\)|' + hljs.UNDERSCORE_IDENT_RE + ')\\s*=>'; + + const FUNCTION_VARIABLE = { + match: [ + /const|var|let/, /\s+/, + IDENT_RE$1, /\s*/, + /=\s*/, + lookahead(FUNC_LEAD_IN_RE) + ], + className: { + 1: "keyword", + 3: "title.function" + }, + contains: [ + PARAMS + ] + }; + + return { + name: 'Javascript', + aliases: ['js', 'jsx', 'mjs', 'cjs'], + keywords: KEYWORDS$1, + // this will be extended by TypeScript + exports: { PARAMS_CONTAINS }, + illegal: /#(?![$_A-z])/, + contains: [ + hljs.SHEBANG({ + label: "shebang", + binary: "node", + relevance: 5 + }), + USE_STRICT, + hljs.APOS_STRING_MODE, + hljs.QUOTE_STRING_MODE, + HTML_TEMPLATE, + CSS_TEMPLATE, + TEMPLATE_STRING, + COMMENT, + NUMBER, + CLASS_REFERENCE, + { + className: 'attr', + begin: IDENT_RE$1 + lookahead(':'), + relevance: 0 + }, + FUNCTION_VARIABLE, + { // "value" container + begin: '(' + hljs.RE_STARTERS_RE + '|\\b(case|return|throw)\\b)\\s*', + keywords: 'return throw case', + relevance: 0, + contains: [ + COMMENT, + hljs.REGEXP_MODE, + { + className: 'function', + // we have to count the parens to make sure we actually have the + // correct bounding ( ) before the =>. There could be any number of + // sub-expressions inside also surrounded by parens. + begin: FUNC_LEAD_IN_RE, + returnBegin: true, + end: '\\s*=>', + contains: [ + { + className: 'params', + variants: [ + { + begin: hljs.UNDERSCORE_IDENT_RE, + relevance: 0 + }, + { + className: null, + begin: /\(\s*\)/, + skip: true + }, + { + begin: /\(/, + end: /\)/, + excludeBegin: true, + excludeEnd: true, + keywords: KEYWORDS$1, + contains: PARAMS_CONTAINS + } + ] + } + ] + }, + { // could be a comma delimited list of params to a function call + begin: /,/, + relevance: 0 + }, + { + match: /\s+/, + relevance: 0 + }, + { // JSX + variants: [ + { begin: FRAGMENT.begin, end: FRAGMENT.end }, + { + begin: XML_TAG.begin, + // we carefully check the opening tag to see if it truly + // is a tag and not a false positive + 'on:begin': XML_TAG.isTrulyOpeningTag, + end: XML_TAG.end + } + ], + subLanguage: 'xml', + contains: [ + { + begin: XML_TAG.begin, + end: XML_TAG.end, + skip: true, + contains: ['self'] + } + ] + } + ], + }, + FUNCTION_DEFINITION, + { + // prevent this from getting swallowed up by function + // since they appear "function like" + beginKeywords: "while if switch catch for" + }, + { + // we have to count the parens to make sure we actually have the correct + // bounding ( ). There could be any number of sub-expressions inside + // also surrounded by parens. + begin: '\\b(?!function)' + hljs.UNDERSCORE_IDENT_RE + + '\\(' + // first parens + '[^()]*(\\(' + + '[^()]*(\\(' + + '[^()]*' + + '\\)[^()]*)*' + + '\\)[^()]*)*' + + '\\)\\s*\\{', // end parens + returnBegin: true, + label: "func.def", + contains: [ + PARAMS, + hljs.inherit(hljs.TITLE_MODE, { begin: IDENT_RE$1, className: "title.function" }) + ] + }, + // catch ... so it won't trigger the property rule below + { + match: /\.\.\./, + relevance: 0 + }, + PROPERTY_ACCESS, + // hack: prevents detection of keywords in some circumstances + // .keyword() + // $keyword = x + { + match: '\\$' + IDENT_RE$1, + relevance: 0 + }, + { + match: [/\bconstructor(?=\s*\()/], + className: { 1: "title.function" }, + contains: [PARAMS] + }, + FUNCTION_CALL, + UPPER_CASE_CONSTANT, + CLASS_OR_EXTENDS, + GETTER_OR_SETTER, + { + match: /\$[(.]/ // relevance booster for a pattern common to JS libs: `$(something)` and `$.something` + } + ] + }; + } + + /* + Language: JSON + Description: JSON (JavaScript Object Notation) is a lightweight data-interchange format. + Author: Ivan Sagalaev + Website: http://www.json.org + Category: common, protocols, web + */ + + function json(hljs) { + const ATTRIBUTE = { + className: 'attr', + begin: /"(\\.|[^\\"\r\n])*"(?=\s*:)/, + relevance: 1.01 + }; + const PUNCTUATION = { + match: /[{}[\],:]/, + className: "punctuation", + relevance: 0 + }; + // normally we would rely on `keywords` for this but using a mode here allows us + // to use the very tight `illegal: \S` rule later to flag any other character + // as illegal indicating that despite looking like JSON we do not truly have + // JSON and thus improve false-positively greatly since JSON will try and claim + // all sorts of JSON looking stuff + const LITERALS = { + beginKeywords: [ + "true", + "false", + "null" + ].join(" ") + }; + + return { + name: 'JSON', + contains: [ + ATTRIBUTE, + PUNCTUATION, + hljs.QUOTE_STRING_MODE, + LITERALS, + hljs.C_NUMBER_MODE, + hljs.C_LINE_COMMENT_MODE, + hljs.C_BLOCK_COMMENT_MODE + ], + illegal: '\\S' + }; + } + + + /** @type LanguageFn */ + function xml(hljs) { + // Element names can contain letters, digits, hyphens, underscores, and periods + const TAG_NAME_RE = concat(/[A-Z_]/, optional(/[A-Z0-9_.-]*:/), /[A-Z0-9_.-]*/); + const XML_IDENT_RE = /[A-Za-z0-9._:-]+/; + const XML_ENTITIES = { + className: 'symbol', + begin: /&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;/ + }; + const XML_META_KEYWORDS = { + begin: /\s/, + contains: [ + { + className: 'keyword', + begin: /#?[a-z_][a-z1-9_-]+/, + illegal: /\n/ + } + ] + }; + const XML_META_PAR_KEYWORDS = hljs.inherit(XML_META_KEYWORDS, { + begin: /\(/, + end: /\)/ + }); + const APOS_META_STRING_MODE = hljs.inherit(hljs.APOS_STRING_MODE, { + className: 'string' + }); + const QUOTE_META_STRING_MODE = hljs.inherit(hljs.QUOTE_STRING_MODE, { + className: 'string' + }); + const TAG_INTERNALS = { + endsWithParent: true, + illegal: /`]+/ + } + ] + } + ] + } + ] + }; + return { + name: 'HTML, XML', + aliases: [ + 'html', + 'xhtml', + 'rss', + 'atom', + 'xjb', + 'xsd', + 'xsl', + 'plist', + 'wsf', + 'svg' + ], + case_insensitive: true, + contains: [ + { + className: 'meta', + begin: //, + relevance: 10, + contains: [ + XML_META_KEYWORDS, + QUOTE_META_STRING_MODE, + APOS_META_STRING_MODE, + XML_META_PAR_KEYWORDS, + { + begin: /\[/, + end: /\]/, + contains: [ + { + className: 'meta', + begin: //, + contains: [ + XML_META_KEYWORDS, + XML_META_PAR_KEYWORDS, + QUOTE_META_STRING_MODE, + APOS_META_STRING_MODE + ] + } + ] + } + ] + }, + hljs.COMMENT( + //, + { + relevance: 10 + } + ), + { + begin: //, + relevance: 10 + }, + XML_ENTITIES, + { + className: 'meta', + begin: /<\?xml/, + end: /\?>/, + relevance: 10 + }, + { + className: 'tag', + /* + The lookahead pattern (?=...) ensures that 'begin' only matches + ')/, + end: />/, + keywords: { + name: 'style' + }, + contains: [TAG_INTERNALS], + starts: { + end: /<\/style>/, + returnEnd: true, + subLanguage: [ + 'css', + 'xml' + ] + } + }, + { + className: 'tag', + // See the comment in the
On this page

Introduction

Scripting in sitespeed.io and Browsertime allows you to measure user journeys by interacting with web pages. This feature is essential for simulating real-user interactions and collecting performance metrics for complex workflows. Scripting works the same in both Browsertime and sitespeed.io.

Simple script

Here's a basic script example to start with:

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) {
+  context.log.info('Start to measure my first URL');
+  return commands.measure.start('https://www.sitespeed.io');
+}
+

To run this script, use the command sitespeed.io -n 1 --multi measure.mjs. This script measures the performance of https://www.sitespeed.io.

You can see that you get two helper objects in that function. The browsertime context and browsertime commands. Lets talk about those helper objects.

Helpers

Lets start with the command object.

The Commands Object

Commands are helpers for interacting with web pages.

Inside your script, you access them as properties on commands.. You can use them to measure a URL like await commands.measure.start('https://www.example.com');. Many of the commands are asynchronous so you need to await them.

You can see all the commands here.

The Context Object

The context object in your script is a help object with access to the current context on when you run Browsertime/sitespeed.io. In most cases you do not need to use them (except the access to the log), but for special use cases they are handy.

The properties on the context object are:

  • options: All options sent from the CLI to Browsertime/sitespeed.io. Here you can fetch paramters that you used when starting the test.
  • log: An instance of the log system. Use it to log what you do.
  • index: The index of the current run.
  • storageManager: The manager that is used to read/store files to disk.
  • selenium.webdriver: The public API object of Selenium WebDriver. You need it if you want to run Selenium scripts.
  • selenium.driver: The instantiated WebDriver for the current browser session.

Asynchronous commands

Many Browsertime commands are asynchronous, returning promises. You can see that when you look in the documencation and see async for the function.

Use await to ensure the script waits for an action to complete. When using multiple async operations, return the last promise to ensure the script waits until all operations are complete.

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) {
+  await commands.navigate('https://www.sitespeed.io');
+  return commands.measure.start('https://www.sitespeed.io/documentation/');
+}
+
\ No newline at end of file diff --git a/docs/documentation/sitespeed.io/scripting/tutorial-02-Running-Scripts.html b/docs/documentation/sitespeed.io/scripting/tutorial-02-Running-Scripts.html new file mode 100644 index 0000000000..e6d46ede8c --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/tutorial-02-Running-Scripts.html @@ -0,0 +1,29 @@ +Tutorial: Running and managing scripts
On this page

Running and managing scripts

Run scripts using Browsertime is easy. Create your script and run it like this:

browsertime myScript.mjs
+

And in sitespeed.io you need to add the --multi switch (test multiple pages).

sitespeed.io myScript.mjs --multi
+

Multiple scripts

For multiple scripts, list them all in the command. This approach helps manage complex scripts by splitting them into multiple files.

sitespeed.io login.mjs measureStartPage.mjs logout.mjs --multi
+

Or you can break out code in multiple files.

Create a file to include exampleInclude.mjs

export async function example() {
+  console.log('This is my example function');
+}
+

Then include it test.mjs:

import { example } from './exampleInclude.mjs';
+export default async function (context, commands) {
+  example();
+}
+

And then run it: sitespeed.io --multi test.mjs

Add meta data to your script

You can add meta data like title and description to your script. The extra data will be visible in the HTML result page.

Setting meta data like this:

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) {
+  commands.meta.setTitle('Test Grafana SPA');
+  commands.meta.setDescription('Test the first page, click the timepicker and then choose <b>Last 30 days</b> and measure that page.');
+  await commands.measure.start(
+    'https://dashboard.sitespeed.io/d/000000044/page-timing-metrics?orgId=1','pageTimingMetricsDefault'
+  );
+  await commands.click.byClassName('gf-timepicker-nav-btn');
+  await commands.wait.byTime(1000);
+  await commands.measure.start('pageTimingMetrics30Days');
+  await commands.click.byLinkTextAndWait('Last 30 days');
+  await commands.measure.stop();
+};
+

Will result in:

Title and description for a script

\ No newline at end of file diff --git a/docs/documentation/sitespeed.io/scripting/tutorial-03-Measurement-Commands.html b/docs/documentation/sitespeed.io/scripting/tutorial-03-Measurement-Commands.html new file mode 100644 index 0000000000..676bb86a29 --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/tutorial-03-Measurement-Commands.html @@ -0,0 +1,76 @@ +Tutorial: Measure
On this page

Measure

In sitespeed.io, measurements can be conducted using a meassure command for page navigation performance, gathering various metrics during page load. Alternatively, the StopWatch command is used for measuring arbitrary durations, such as the time taken for certain actions to complete. When you create your script you need to know what you want to measure.

The measure Command

In web performance, "navigation" means switching from one webpage to another. This switch triggers several steps in your browser, like closing the current page, loading new content, and showing the new page fully loaded. In sitespeed.io, the commands.measure function is used to analyze this process. It tracks various performance details from the moment you start moving to a new page until it's completely loaded. This includes how long the page takes to load, how fast resources (like images and scripts) are loaded, and more, giving you a full picture of the navigation's performance.

Use the measure command for measuring page navigation performance. It captures metrics from the start to the end of a page load.

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) {
+  return commands.measure.start('https://www.sitespeed.io');
+}
+

If you use the measure command without a URL, it sets everything up and starts recording a video, but it doesn't navigate to a new page automatically. You need to manually navigate to a page, click a link, or submit a form. Remember to stop the measurement when you're done, so Browsertime/sitespeed.io can gather and report the performance data.

For example, to measure how long it takes to go from the sitespeed.io homepage to its documentation page, you would start by navigating to the sitespeed.io homepage. Then, you manually click on the link to the documentation page. Once you're on the documentation page, you stop the measurement to capture the performance metrics of this navigation.

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) {
+  
+  await commands.navigate('https://www.sitespeed.io');
+
+  await commands.measure.start('Documentation');
+  // Using a xxxAndWait command will make the page wait for a navigation
+  await commands.click.byLinkTextAndWait('Documentation');
+  return commands.measure.stop();
+}
+

The measure command.

The stopWatch Command

The Stop Watch command in sitespeed.io is used for measuring the time of activities other than web page navigation, like specific processes or user actions. You manually start and stop this watch to track the duration of these actions. When you use the Stop Watch, its timing data gets automatically linked to the web page you were analyzing right before you started the watch. This way, the time recorded by the Stop Watch becomes part of the performance data for that particular page.

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) {
+  const stopWatch = commands.stopWatch.get('Before_navigating_page');
+  // Do the thing you want to measure ...
+  // Then stop the watch 
+  const time = stopWatch.stop();
+  // Measure navigation to a page
+  await commands.measure.start('https://www.sitespeed.io');
+  // Then attach that timing to that page.
+  commands.measure.add(stopWatch.getName(), time);
+}
+
+

The stop watch command.

Using user timings and element timings API

When testing a webpage you manage, it's a good idea to use the User Timing and Element Timing APIs built into browsers. Most browsers support the User Timing API, and Chrome-based browsers support the Element Timing API. Browsertime and sitespeed.io automatically collect metrics from these APIs when you execute the measure command. If you need these metrics after navigating to a different page, you can also retrieve them using the JavaScript command within your script. This approach is helpful for gathering detailed timing information related to specific elements or user-defined timings on your page.

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) {
+  await commands.navigate('https://www.sitespeed.io');
+
+  // The sitespeed.io start page has a user timing mark named userTimingHeader
+  const userTimingHeader = await commands.js.run(
+    `return performance.getEntriesByName('userTimingHeader')[0].startTime;`
+  );
+
+  // The sitespeed.io start page has a element timing api for the logo
+  const logoRenderTime = await commands.js.run(`
+  const observer = new PerformanceObserver(list => {});
+  observer.observe({ type: 'element', buffered: true });
+  const entries = observer.takeRecords();
+  for (let entry of entries) {
+    if (entry.identifier === 'logo') {
+      return Number(entry.renderTime.toFixed(0));
+    }
+  }
+  `);
+
+  context.log.info(
+    `User Timing header: ${userTimingHeader} ms  and Logo Element render time ${logoRenderTime} ms`
+  );
+}
+

Measure a single page application (SPA)

Single Page Applications (SPAs) are web applications that load a single HTML page and dynamically update that page as the user interacts with the app. Unlike traditional web applications that reload the entire page or load new pages to display different content, SPAs rewrite the current page in response to user actions (a soft navigation), in best cases it s leading to a more fluid user experience.

At the moment browser based metrics like first paint, largest contentful paint and others aren't updated for soft navigations. The Chrome team is working on that and we will try to implement that when its more mature. You can follow that work in Browsertime issue #2000.

The best way to measure a SPA today is with the visual metrics that Browsertime collects by analysing the video recording. That way you can get metrics like first visual change and last visual change.

Here's an example on how to do that:

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) {
+  await commands.measure.start('https://react.dev');
+  await commands.measure.start('Learn');
+  await commands.mouse.singleClick.byLinkTextAndWait('Learn');
+  return commands.measure.stop();
+}
+

And run it like this: sitespeed.io react.mjs --multi --spa --video --visualMetrics

If you are testing a SPA it's important to add the --spa switch. That helps Browsertime to setup some extra functionality to test that page (like instead of waiting for the onloadEvent to stop the measurement, it waits for silence in the network log).

The Chrome team has the following defintion of a Soft navigation:

  1. The navigation is initiated by a user action.
  2. The navigation results in a visible URL change to the user, and a history change.
  3. The navigation results in a DOM change.

For Browsertime it's important that the URL change, that's how we know that we are measuruing a new page.

If your page do not change the URL or load any resources (JSON/JavaScript/CSS or images) when you do the "soft" navigation then you need to use the stop watch to measure that kind of navigation.

\ No newline at end of file diff --git a/docs/documentation/sitespeed.io/scripting/tutorial-04-Interact-with-the-page.html b/docs/documentation/sitespeed.io/scripting/tutorial-04-Interact-with-the-page.html new file mode 100644 index 0000000000..c7f40723ea --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/tutorial-04-Interact-with-the-page.html @@ -0,0 +1,68 @@ +Tutorial: Interact with the page
On this page

Interact with the page

There are multiple ways to interact with the current page. We have tried to add the most common ways so you don't need to use Selenium directly, and if yoiu think something is missing, please create an issue.

Finding elements

One of the key things in your script is to be able to find the right element to invoke. If the element has an id it’s easy. If not you can use developer tools in your favourite browser. The all work mostly the same: Open DevTools in the page you want to inspect, click on the element and right click on DevTools for that element. Then you will see something like this:

Using Safari to find the CSS Selector to the element

Using Safari to find the selector

Using Firefox to find the CSS Selector to the element

Using Firefox to find the selector{

Using Chrome to find the CSS Selector to the element

Using Chrome to find the selector

Using Actions

Since Browsertime 21.0.0 we support easier access to the Selenium Action API. That makes easier to interact with the page and you can also chain commands. You can checkout the Selenium NodeJS Action API to see more what you can do.

Here's an example doing search on Wikipedia:

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) {
+  await commands.measure.start('https://www.wikipedia.org');
+  const searchBox = await commands.element.getById('searchInput');
+  const submitButton = await commands.element.getByClassName(
+    'pure-button pure-button-primary-progressive'
+  );
+
+  await commands.measure.start('Search');
+  await commands.action
+    .getActions()
+    .move({ origin: searchBox })
+    .pause(1000)
+    .press()
+    .sendKeys('Hepp')
+    .pause(200)
+    .click(submitButton)
+    .perform();
+
+  // If you would do more actions after calling .perform()
+  // you manually need to clear the action API
+  //await commands.action.clear();
+
+  await commands.wait.byPageToComplete();
+  return commands.measure.stop();
+}
+

JavaScript

You can run your own JavaScript in the browser from your script. This is powerful because that makes it possible to do whatever you want :)

Run

Run JavaScript. Will throw an error if the JavaScript fails.

If you want to get values from the page, this is your best friend. Make sure to return the value and you can use it in your script.

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) {
+  // We are in browsertime context so you can skip that from your options object
+  const secretValue = await commands.js.run('return 12');
+  // if secretValue === 12 ...
+}
+

By default this will return a Selenium WebElement.

Run and wait on page

Run JavaScript and wait for page complete check. Do that with commands.js.runAndWait("").

Click

The click command finds an element and runs .click() on the element.

Click commands have two different versions: One that will return a promise when the link has been clicked and one that will return a promise that will be fullfilled when the link has been clicked and the browser navigated to the new URL and the page complete check is done.

If it does not find the link, it will throw an error, so make sure to catch it if you want an alternative flow.

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) {
+  await commands.navigate('https://www.sitespeed.io/');
+  try {
+    await commands.click.byLinkText('Documentation');
+    await commands.click.byLinkTextAndWait('Documentation');
+  } catch(error) {
+    context.log.error('Could not find the link text "Documentation"', error);
+  }
+}
+

All click commands

You can find all the click commands here.

Wait

There are a couple of wait commands that makes it easier to wait. Either you can wait on a specific id to appear, for x amount of milliseconds or for a page to finish loading.

Mouse

The mouse command will perform various mouse events using the Seleniums Action API.

Move

The mouse move commands.

Single click

The single click commands.

Double click

The double click commands.

Context click

The context click commands.

Click and hold

The click and hold commands.

Scroll

You can use scroll commands to scroll the browser window.

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) {
+  const delayTime = 250;
+
+  await commands.measure.start();
+  await commands.navigate(
+    'https://www.sitespeed.io/documentation/sitespeed.io/performance-dashboard/'
+  );
+  await  commands.scroll.toBottom(delayTime);
+  return commands.measure.stop();
+};
+

Add text

You can add text to input elements. The element needs to visible. You can also send pressable keys as Unicode PUA (PrivateUser Area) format.

The add text command.

Switch

You can switch to frames/windows or tabs using the the switch commands.

Set

Using the set commands you can set values to HTML elements.

Select

You can use the select command for selecting an option in a drop-down field.

\ No newline at end of file diff --git a/docs/documentation/sitespeed.io/scripting/tutorial-05-Interact-Browser.html b/docs/documentation/sitespeed.io/scripting/tutorial-05-Interact-Browser.html new file mode 100644 index 0000000000..4a7aed7585 --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/tutorial-05-Interact-Browser.html @@ -0,0 +1,31 @@ +Tutorial: Interact with the browser
On this page

Interact with the browser

You can navigate to a URL without measuring it. You do it with the navigate function. Navigation will use the same logic as measuring, it will wait for the page complete check to finish.

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) {
+  await commands.navigate('https://www.sitespeed.io');
+}
+

Cache

You can clear the browser cache from your script. The command works in Chrome, Edge and Firefox. Use it when you want to clear the browser cache between different URLs.

Clear cache and cookies

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) {
+  // First you probably visit a couple of pages and then clear the cache
+  await commands.cache.clear();
+  // And then visit another page
+}
+

Clear cache but keep cookies

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) {
+  // If you have login cookies that lives really long you may want to test accesing the page as a logged in user
+  // but without a browser cache. You can try that with ...
+
+  // Login the user and the clear the cache but keep cookies
+  await commands.cache.clearKeepCookies();
+  // and then access the URL you wanna test.
+}
+

You can use the Navigation command to go back, forward or refresh the page in the browser.

\ No newline at end of file diff --git a/docs/documentation/sitespeed.io/scripting/tutorial-06-Error-handling.html b/docs/documentation/sitespeed.io/scripting/tutorial-06-Error-handling.html new file mode 100644 index 0000000000..24c3445045 --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/tutorial-06-Error-handling.html @@ -0,0 +1,48 @@ +Tutorial: Error handling
On this page

Error handling

You can try/catch failing commands that throw errors. If an error is not caught in your script, it will be caught in sitespeed.io and the error will be logged and reported in the HTML and to your data storage (Graphite/InfluxDb) under the key browsertime.statistics.errors.

If you do catch the error, you should make sure you report it yourself with the error function, so you can see that in the HTML. This is needed for all errors except navigating/measuring a URL. They will automatically be reported (since they are always important).

Here's an example of catching a URL that don't work and still continue to test another one. Remember since a navigation fails, this will be reported automatically and you don't need to do anything.

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) {
+  await commands.measure.start('https://www.sitespeed.io');
+  try {
+    await commands.measure.start('https://nonworking.url/');
+  } catch (e) {}
+  return commands.measure.start('https://www.sitespeed.io/documentation/');
+};
+

You can also create your own errors. The error will be reported in the HTML and sent to Graphite/InfluxDB. If you report an error, the exit code from sitespeed.io will be > 0.

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) {
+  // ...
+  try {
+    // Click on a link
+    await commands.click.byLinkTextAndWait('Checkout');
+  } catch (e) {
+    // Oh no, the content team has changed the name of the link!
+     commands.error('The link named Checkout do not exist on the page');
+    // Since the error is reported, you can alert on it in Grafana
+  }
+};
+

Failure

You can mark your test as a failure. If a test is marked as a failure, the exit code from Browsertime/sitespeed.io will be larger than zero.

await commands.markAsFailure('My test failed');
+// Or if you want to set the exit code
+// it works the same way
+process.exitCode = 1;
+

Then yoy

$ sitespeed.io --multi myJourney.mjs
+...
+$ echo $?
+1
+

Screenshot

Take a screenshot. The image is stored in the screenshot directory for the URL you are testing. This can be super helpful to use in a catch block if something fails. If you use sitespeed.io you can find the image in the screenshot tab for each individual run.

Screenshots

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) {
+  try {
+    // Doing something that fails
+  } catch(error) {
+    await commands.screenshot.take('my-failure');
+  }
+ }
+
\ No newline at end of file diff --git a/docs/documentation/sitespeed.io/scripting/tutorial-07-Debugging-Scripts.html b/docs/documentation/sitespeed.io/scripting/tutorial-07-Debugging-Scripts.html new file mode 100644 index 0000000000..c24c35ba12 --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/tutorial-07-Debugging-Scripts.html @@ -0,0 +1,33 @@ +Tutorial: Debugging scripts
On this page

Debugging scripts

There are a couple of ways of debugging your scripts.

Use the log

The easist way know what's going on is log to the browsertime/sitespeed.io log. You can do that with log object that exist in the context object.

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) {
+  context.log.info('Logging at the info level');
+  context.log.error('Oh no it is an error!');
+ }
+

Use debug with breakpoints

You can use breakpoints to debug your script. You can add breakpoints to your script that will be used when you run in --debug mode. At each breakpoint the browser will pause. You can continue by adding window.browsertime.pause=false; in your developer console.

Debug mode works in Chrome/Firefox/Edge when running on desktop. It do not work in Docker and on mobile. When you run in debug mode, devtools will be automatically open so you can debug your script.

In debug mode, the browser will pause after each iteration.

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) {
+  await commands.measure.start('https://www.sitespeed.io');
+  await commands.breakpoint('');
+  return commands.measure.start('https://www.sitespeed.io/documentation/');
+};
+

Watch what's going on

If your script stopped working in your monitoring, try to run the test locally where you can watch the browser window.

Either you can do it with Docker following these instructions or run your test with a locally installed sitespeed.io using npm install sitespeed.io -g.

Better safe than sorry

Implement try/catch blocks for robust error handling.

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) {
+  try {
+    // do something that can break
+  } catch(error) {
+    context.log.error('Oh no it is an error!', error);
+    // You can also take a screenshot of the error
+    await commands.screenshot.take('myError');
+  }
+ }
+
\ No newline at end of file diff --git a/docs/documentation/sitespeed.io/scripting/tutorial-08-Setting-Up-IntelliSense.html b/docs/documentation/sitespeed.io/scripting/tutorial-08-Setting-Up-IntelliSense.html new file mode 100644 index 0000000000..08c8692d6d --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/tutorial-08-Setting-Up-IntelliSense.html @@ -0,0 +1,11 @@ +Tutorial: Code completion and IntelliSense
On this page

Code completion and IntelliSense

IntelliSense in Visual Studio Code can significantly enhance your scripting experience with Browsertime by providing code completions, parameter info, quick info, and member lists. Here's how to set it up:

Install Browsertime

First, ensure that the Browsertime types are installed in your project. If they're not included by default, you can install them via npm:

   npm install browsertime --save-dev
+

Reload Visual Studio Code

After these changes, reload Visual Studio Code to ensure that the settings are applied.

Write Your Script

Now, when you write your Browsertime script, IntelliSense should automatically suggest relevant Browsertime methods and properties when you add the param comments as in this example.

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) { 
+
+};
+
\ No newline at end of file diff --git a/docs/documentation/sitespeed.io/scripting/tutorial-09-Examples.html b/docs/documentation/sitespeed.io/scripting/tutorial-09-Examples.html new file mode 100644 index 0000000000..103d225841 --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/tutorial-09-Examples.html @@ -0,0 +1,307 @@ +Tutorial: Examples
On this page

Examples

Here are some examples on how you can use the scripting capabilities.

Measure multiple pages

Test multiple pages in a script:

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) {
+  await commands.measure.start('https://www.sitespeed.io');
+  await commands.measure.start('https://www.sitespeed.io/examples/');
+  return commands.measure.start('https://www.sitespeed.io/documentation/');
+};
+

Measure multiple pages and start white

Sometimes recording a video and measuring multiple pages you will see that the layout is kept in the browser until the first paint of the new page. You can hack that by removing the current body and set the background color to white. Then every video will start white.

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) {
+    await commands.measure.start('https://www.sitespeed.io');
+    await commands.js.run('document.body.innerHTML = ""; document.body.style.backgroundColor = "white";');
+    await commands.measure.start('https://www.sitespeed.io/examples/');
+    await commands.js.run('document.body.innerHTML = ""; document.body.style.backgroundColor = "white";');
+    return commands.measure.start('https://www.sitespeed.io/documentation/');
+};
+

Measuring Interaction to next paint - INP

One of the new metrics Google is pushing is Interaction to next paint. You can use it when you collect RUM and using sitespeed.io. To measure it you need to interact with a web page. The best way to do that is using the Action API.

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) {
+  // Start to measure
+  await commands.measure.start();
+  // Go to a page ...
+  await commands.navigate('https://en.m.wikipedia.org/wiki/Barack_Obama');
+
+  // When the page has finished loading you can find the navigation and click on it
+  const element = await commands.element.getByXpath(
+    '//*[@id="mw-mf-main-menu-button"]'
+  );
+  await commands.action.getActions().click(element).perform();
+  
+  // If you want to do multiple actions, remember to clear() the Action API manually
+
+  // Add some wait for the menu to show up
+  await commands.wait.byTime(2000);
+
+  // Measure everything, that means you will run the JavaScript that collects the interaction to next paint
+  return commands.measure.stop();
+}
+

You will see the metric in the page summary and in the metrics section.

Measure a login step

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) {
+  // Navigate to a URL, but do not measure the URL
+  await commands.navigate(
+    'https://en.wikipedia.org/w/index.php?title=Special:UserLogin&returnto=Main+Page'
+  );
+
+  try {
+    // Add text into an input field, finding the field by id
+    await commands.addText.byId('login', 'wpName1');
+    await commands.addText.byId('password', 'wpPassword1');
+
+    // Start the measurement and give it the alias login
+    // The alias will be used when the metrics is sent to
+    // Graphite/InfluxDB
+    await commands.measure.start('login');
+
+    // Find the submit button and click it and wait for the
+    // page complete check to finish on the next loaded URL
+    await commands.click.byIdAndWait('wpLoginAttempt');
+    // Stop and collect the metrics
+    return commands.measure.stop();
+  } catch (e) {
+    // We try/catch so we will catch if the the input fields can't be found
+    // The error is automatically logged in Browsertime an rethrown here
+    // We could have an alternative flow ...
+    // else we can just let it cascade since it caught later on and reported in
+    // the HTML
+    throw e;
+  }
+};
+

Measure the login step and more

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) {
+  // We start by navigating to the login page.
+  await commands.navigate(
+    'https://en.wikipedia.org/w/index.php?title=Special:UserLogin&returnto=Main+Page'
+  );
+
+  // When we fill in a input field/click on a link we wanna
+  // try/catch that if the HTML on the page changes in the feature
+  // sitespeed.io will automatically log the error in a user friendly
+  // way, and the error will be re-thrown so you can act on it.
+  try {
+    // Add text into an input field, finding the field by id
+    await commands.addText.byId('login', 'wpName1');
+    await commands.addText.byId('password', 'wpPassword1');
+
+    // Start the measurement before we click on the
+    // submit button. Sitespeed.io will start the video recording
+    // and prepare everything.
+    await commands.measure.start('login');
+    // Find the sumbit button and click it and then wait
+    // for the pageCompleteCheck to finish
+    await commands.click.byIdAndWait('wpLoginAttempt');
+    // Stop and collect the measurement before the next page we want to measure
+    await commands.measure.stop();
+    // Measure the Barack Obama page as a logged in user
+    await commands.measure.start(
+      'https://en.wikipedia.org/wiki/Barack_Obama'
+    );
+    // And then measure the president page
+    return commands.measure.start('https://en.wikipedia.org/wiki/President_of_the_United_States');
+  } catch (e) {
+    // We try/catch so we will catch if the the input fields can't be found
+    // The error is automatically logged in Browsertime and re-thrown here
+    // We could have an alternative flow ...
+    // else we can just let it cascade since it caught later on and reported in
+    // the HTML
+    throw e;
+  }
+};
+

Measure one page after you logged in

Testing a page after you have logged in: First create a script that logs in the user (login.mjs):

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) {
+  await commands.navigate(
+    'https://en.wikipedia.org/w/index.php?title=Special:UserLogin&returnto=Main+Page'
+  );
+
+  try {
+    await commands.addText.byId('login', 'wpName1');
+    await commands.addText.byId('password', 'wpPassword1');
+    // Click on the submit button with id wpLoginAttempt
+    await commands.click.byIdAndWait('wpLoginAttempt');
+    // wait on a specific id to appear on the page after you logged in
+    return commands.wait.byId('pt-userpage', 10000);
+  } catch (e) {
+    // We try/catch so we will catch if the the input fields can't be found
+    // The error is automatically logged in Browsertime and re-thrown here
+    // We could have an alternative flow ...
+    // else we can just let it cascade since it caught later on and reported in
+    // the HTML
+    throw e;
+  }
+};
+

Then access the page that you want to test:

sitespeed.io --preScript login.mjs https://en.wikipedia.org/wiki/Barack_Obama
+

A more complicated login example

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) {
+  await commands.navigate(
+    'https://example.org'
+  );
+  try {
+    // Find the sign in button and click it
+    await commands.click.byId('sign_in_button');
+    // Wait some time for the page to open a new login frame
+    await commands.wait.byTime(2000);
+    // Switch to the login frame
+    await commands.switch.toFrame('loginFrame');
+    // Find the username fields by xpath (just as an example)
+    await commands.addText.byXpath(
+      'peter@example.org',
+      '//*[@id="userName"]'
+    );
+    // Click on the next button
+    await commands.click.byId('verifyUserButton');
+    // Wait for the GUI to display the password field so we can select it
+    await commands.wait.byTime(2000);
+    // Wait for the actual password field
+    await commands.wait.byId('password', 5000);
+    // Fill in the password
+    await commands.addText.byId('dejh8Ghgs6ga(1217)', 'password');
+    // Click the submit button
+    await commands.click.byId('btnSubmit');
+    // In your implementation it is probably better to wait for an id
+    await commands.wait.byTime(5000);
+    // Measure the next page as a logged in user
+    return  commands.measure.start(
+      'https://example.org/logged/in/page'
+  );
+  } catch(e) {
+    // We try/catch so we will catch if the the input fields can't be found
+    // We could have an alternative flow ...
+    // else we can just let it cascade since it caught later on and reported in
+    // the HTML
+    throw e;
+  }
+};
+

Scroll the page

You can scroll the page to trigger metrics. To get the Cumulative Layout Shift metric for Chrome closer to what real users get you can scroll the page and measure that. Depending on how your page work, you may want to tune the delay between the scrolling.

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) {
+  const delayTime = 250;
+
+  await commands.measure.start();
+  await commands.navigate(
+    'https://www.sitespeed.io/documentation/sitespeed.io/performance-dashboard/'
+  );
+  await  commands.scroll.toBottom(delayTime);
+  return commands.measure.stop();
+};
+

Add your own metrics

You can add your own metrics by adding the extra JavaScript that is executed after the page has loaded BUT did you know that also can add your own metrics directly through scripting? The metrics will be added to the metric tab in the HTML output and automatically sent to Graphite/InfluxDB.

In this example we collect the temperature from our Android phone that runs the tests:

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) {
+  // Get the temperature from the phone
+  const temperature = await commands.android.shell("dumpsys battery | grep temperature | grep -Eo '[0-9]{1,3}'");
+  // Start the test
+  await commands.measure.start(
+    'https://www.sitespeed.io'
+  );
+  // This is the magic where we add that new metric. It needs to happen
+  // after measure.start so we know where that metric belong
+  commands.measure.add('batteryTemperature', temperature/10);
+};
+

In this example we collect the number of comments on a blog post using commands.js.run() to collect an element, use regex to parse out the number, and add it back as a custom metric.

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) {
+   await commands.measure.start('blog-post'); //alias is now blog-post
+   await commands.navigate('https://www.exampleBlog/blog-post');
+   
+   //use commands.js.run to return the element using pure javascript
+   const element = await commands.js.run('return(document.getElementsByClassName("comment-count")[0].innerText)'); 
+   
+   //parse out just the number of comments
+   var elementMetric = element.match(/\d/)[0];
+  
+   // need to stop the measurement before you can add it as a metric
+   await commands.measure.stop();
+   
+   // metric will now be added to the html and outpout to graphite/influx if you're using it
+   await commands.measure.add('commentsCount', elementMetric);
+};
+

Measure a checkout process

One of the really cool things with scripting is that you can measure all the pages in a checkout process. This is an example shop where you put one item in your cart and checkout as a guest.

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) {
+  // Start by measuring the first page of the shop
+  await commands.measure.start('https://shop.example.org');
+
+  // Then the product page
+  // Either your shop has a generic item used for testing that you can use
+  // or in real life you maybe need to add a check that the item really exists in stock
+  // and if not, try another product
+  await commands.measure.start('https://shop.example.org/prodcucs/theproduct');
+
+  // Add the item to your cart
+  await commands.click.bySelector('.add-to-cart');
+
+  // Go to the cart (and measure it)
+  await commands.measure.start('https://shop.example.org/cart/');
+
+  // Checkout as guest but you could also login as a customer
+  // We hide the HTML to avoid that the click on the link will
+  // fire First Visual Change. Best case you don't need to but we
+  // want an complex example
+  await commands.js.run('for (let node of document.body.childNodes) { if (node.style) node.style.display = "none";}');
+  await commands.measure.start('CheckoutAsGuest');
+  await commands.click.bySelectorAndWait('.checkout-as-guest');
+  // Make sure to stop measuring and collect the metrics for the CheckoutAsGuest step
+  await commands.measure.stop();
+
+  // Finish your checkout
+  await commands.js.run('document.body.style.display = "none"');
+  await commands.measure.start('FinishCheckout');
+  await commands.click.bySelectorAndWait('.checkout-finish');
+  // And collect metrics for the FinishCheckout step
+  return commands.measure.stop();
+  // In a real web shop you probably can't finish the last step or you can return the item
+  // so the stock is correct. Either you do that at the end of your script or you
+  // add the item id in the context object like context.itemId = yyyy. Then in your
+  // postScript you can do what's needed with that id.
+};
+

Test multiple URLs

If you want to test multiple URLs and need to do some specific things before each URL, you can do something like this (we pass on our own options to the script):

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+module.exports = async function (context, commands) {
+  const urls = context.options.urls;
+  for (let url of urls) {
+   // Do the stuff for each url that you need to do
+   // Maybe login a user or add a cookie or something
+   // Then test the URL
+   await commands.measure.start(url);
+   // When the test is finished, clear the browser cache
+   await commands.cache.clear();
+   // Navigate to a blank page so you kind of start from scratch for the next URL
+   await commands.navigate('about:blank');
+  }
+};
+

Then run your tests like this:

sitespeed.io testMultipleUrls.js --multi --browsertime.urls https://www.sitespeed.io --browsertime.urls https://www.sitespeed.io/documentation -n 1
+

Or if you use JSON configuration, the same configuration looks like this:

{ 
+  "browsertime": {
+    "urls": ["url1", "url2", "url3"]
+  }
+}
+
\ No newline at end of file diff --git a/docs/documentation/sitespeed.io/scripting/tutorial-10-Selenium.html b/docs/documentation/sitespeed.io/scripting/tutorial-10-Selenium.html new file mode 100644 index 0000000000..ab5da4b8cb --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/tutorial-10-Selenium.html @@ -0,0 +1,40 @@ +Tutorial: Running Selenium code
On this page

Running Selenium code

You can use Selenium directly if you need to use things that are not available through our commands. We use the NodeJS flavor of Selenium.

You get a hold of the Selenium objects through the context object.

The selenium.webdriver is the Selenium WebDriver public API object. And selenium.driver is the instantiated version of the WebDriver driving the current version of the browser.

Checkout this example to see how you can use them.

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) {
+  // We fetch the selenium webdriver from context
+  // The selenium-webdriver 
+  // https://www.selenium.dev/selenium/docs/api/javascript/module/selenium-webdriver/index.html
+  const seleniumWebdriver = context.selenium.webdriver;
+  // The driver exposes for example By that you use to find elements
+  const By = seleniumWebdriver.By;
+
+  // We use the driver to find an element
+  const seleniumDriver = context.selenium.driver;
+
+  // To navigate to a new page it is best to use our navigation commands
+  // so the script waits until the page is loaded
+  await commands.navigate('https://www.sitespeed.io');
+
+  // Lets use Selenium to find the Documentation link
+  const seleniumElement = await seleniumDriver.findElement(By.linkText('Documentation'));
+  
+  // So now we actually got a Selenium WebElement 
+  // https://www.selenium.dev/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_WebElement.html
+  context.log.info('The element tag is ', await seleniumElement.getTagName());
+
+  // We then use our command to start a measurement
+  await commands.measure.start('DocumentationPage');
+
+  // Use the Selenium WebElement and click on it
+  await seleniumElement.click();
+  // We make sure to wait for the new page to load
+  await commands.wait.byPageToComplete();
+
+  // Stop the measuerment
+  return commands.measure.stop();
+}
+

If you need help with Selenium, checkout the official Selenium documentation.

\ No newline at end of file diff --git a/docs/documentation/sitespeed.io/scripting/tutorial-11-Chrome-Devtools-Protocol.html b/docs/documentation/sitespeed.io/scripting/tutorial-11-Chrome-Devtools-Protocol.html new file mode 100644 index 0000000000..2ffff1c4ad --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/tutorial-11-Chrome-Devtools-Protocol.html @@ -0,0 +1,71 @@ +Tutorial: Chrome Devtools Protocol (CDP)
On this page

Chrome Devtools Protocol (CDP)

Send messages to Chrome using the Chrome DevTools Protocol. This only works in Chrome/Edge. You can send, send and get and listen on events. This is a super powerful feature that enables you to do almost whatever you want with the browser.

Sending a command

Send a command to Chrome and don’t expect something back.

Here’s an example of injecting JavaScript that runs on every new document.

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) {
+  await commands.cdp.send('Page.addScriptToEvaluateOnNewDocument',{source: 'console.log("hello");'});
+  await commands.measure.start('https://www.sitespeed.io');
+}
+

Send and get something back

Send a command to Chrome and get the result back.

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) {
+  await commands.measure.start('https://www.sitespeed.io');
+  const domCounters = await commands.cdp.sendAndGet('Memory.getDOMCounters');
+  context.log.info('Memory.getDOMCounters %j', domCounters);
+ }
+

Listen on events

Here’s an example to get hold of all responses for a page.

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) {
+  const responses = [];
+  await commands.cdp.on('Network.responseReceived', params => {
+    responses.push(params);
+  });
+  await commands.measure.start('https://www.sitespeed.io/search/');
+  context.log.info('Responses %j', responses);
+};
+

Use the raw CDP client

Under the hood Browsertime uses the chrome-remote-interface. If you need you can get the raw CDP client so you can do whatever you want. Here’s an example on how to change the server header on the response.

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) {
+  const cdpClient = commands.cdp.getRawClient();
+  await cdpClient.Fetch.enable({
+    patterns: [
+      {
+        urlPattern: '*',
+        requestStage: 'Response'
+      }
+    ]
+  });
+
+  cdpClient.Fetch.requestPaused(async reqEvent => {
+    const { requestId } = reqEvent;
+    let responseHeaders = reqEvent.responseHeaders || [];
+
+    const newServerHeader = { name: 'server', value: 'Haxxor' };
+    const foundHeaderIndex = responseHeaders.findIndex(
+      h => h.name === 'server'
+    );
+    if (foundHeaderIndex) {
+      responseHeaders[foundHeaderIndex] = newServerHeader;
+    } else {
+      responseHeaders.push(newServerHeader);
+    }
+
+    return cdpClient.Fetch.continueResponse({
+      requestId,
+      responseCode: 200,
+      responseHeaders
+    });
+  });
+
+  return commands.measure.start('https://www.sitespeed.io/search/');
+}
+
+
\ No newline at end of file diff --git a/docs/documentation/sitespeed.io/scripting/tutorial-12-Android.html b/docs/documentation/sitespeed.io/scripting/tutorial-12-Android.html new file mode 100644 index 0000000000..acb2bc3cf2 --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/tutorial-12-Android.html @@ -0,0 +1,29 @@ +Tutorial: Android devices
On this page

Android devices

Testing on an Android device should work the same way as testing on desktop, as long as you setup your device following the instructions.

Run shell command

If you run your tests on an Android phone you can interact with your phone through the shell.

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) {
+  // Get the temperature from the phone
+  const temperature = await commands.android.shell("dumpsys battery | grep temperature | grep -Eo '[0-9]{1,3}'");
+  context.log.info('The battery temperature is %s', temperature/10);
+  // Start the test
+  return commands.measure.start(
+    'https://www.sitespeed.io'
+  );
+};
+

Run shell command as root

If you rooted your device and want to run as root.

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) {
+  // Get the temperature from the phone
+  const temperature = await commands.android.shellAsRoot("dumpsys battery | grep temperature | grep -Eo '[0-9]{1,3}'");
+  context.log.info('The battery temperature is %s', temperature/10);
+  // Start the test
+  return commands.measure.start(
+    'https://www.sitespeed.io'
+  );
+};
+
\ No newline at end of file diff --git a/docs/documentation/sitespeed.io/scripting/tutorial-13-Tips-and-tricks.html b/docs/documentation/sitespeed.io/scripting/tutorial-13-Tips-and-tricks.html new file mode 100644 index 0000000000..a1e5be5f6b --- /dev/null +++ b/docs/documentation/sitespeed.io/scripting/tutorial-13-Tips-and-tricks.html @@ -0,0 +1,110 @@ +Tutorial: Tips and tricks
On this page

Tips and tricks

Here are some tips and tricks that can make your scripting better.

Include the script in the HTML result

If you wanna keep of what script you are running, you can include the script into the HTML result with --html.showScript. You will then get a link to a page that show the script.

Page to page

Getting correct Visual Metrics

Visual metrics is the metrics that are collected using the video recording of the screen. In most cases that will work just out of the box. One thing to know is that when you go from one page to another page, the browser keeps the layout of the old page. That means that your video will start with the first page (instead of white) when you navigate to the next page.

It will look like this: Page to page

This is perfectly fine in most cases. But if you want to start white (the metrics somehow isn't correct) or if you click a link and that click changes the layout and is caught as First Visual Change, there are workarounds.

If you just want to start white and navigate to the next page you can just clear the HTML between pages:

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) {
+    await commands.measure.start('https://www.sitespeed.io');
+    // Renove the HTML and make sure the background is white
+    await commands.js.run('document.body.innerHTML = ""; document.body.style.backgroundColor = "white";');
+    return commands.measure.start('https://www.sitespeed.io/examples/');
+};
+

If you want to click a link and want to make sure that the HTML doesn't change when you click the link, you can try to hide the HTML and then click the link.

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) {
+    await commands.measure.start('https://www.sitespeed.io');
+    // Hide everything
+    // We do not hide the body since the body needs to be visible when we do the magic to find the staret of the
+    // navigation by adding a layer of orange on top of the page
+    await commands.js.run('for (let node of document.body.childNodes) { if (node.style) node.style.display = "none";}');
+    // Start measurning
+    await commands.measure.start();
+    // Click on the link and wait on navigation to happen
+    await commands.click.bySelectorAndWait('body > nav > div > div > div > ul > li:nth-child(2) > a');
+    return commands.measure.stop();
+};
+

Pass your own options to your script

You can add your own parameters to the options object (by adding a parameter) and then pick them up in the script. The scripts runs in the context of browsertime, so you need to pass it in via that context.

For example: you wanna pass on a password to your script, you can do that by adding --browsertime.my.password MY_PASSWORD and then in your code get a hold of that with:

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) {
+  // We are in browsertime context so you can skip that from your options object
+  context.log.info(context.options.my.password);
+};
+

If you use a configuration file you can pass on options like this:

{
+    "browsertime": {
+        "my": {
+            "password": "paAssW0rd"
+        }
+    }
+}
+

Getting values from your page

In some scenarios you want to do different things dependent on what shows on your page. For example: You are testing a shop checkout and you need to verify that the item is in stock. You can run JavaScript and get the value back to your script.

Here's an simple example, IRL you will need to get something from the page:

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) {
+  // We are in browsertime context so you can skip that from your options object
+  const secretValue = await commands.js.run('return 12');
+  // if secretValue === 12 ...
+}
+

If you want to have different flows depending on a element exists you can do something like this:

...
+const exists = await commands.js.run('return (document.getElementById("nonExistsingID") != null) ');
+if (exists) {
+    // The element with that id exists
+} else {
+    // There's no element with that id
+}
+

Test one page that need a much longer page complete check than others

If you have one page that needs some special handling that maybe do a couple of late and really slow AJAX requests, you can catch that with your on wait for the page to finish.

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) {
+  // First test a couple pages with default page complete check
+  await commands.measure.start('https://<page1>');
+  await commands.measure.start('https://<page2>');
+  await commands.measure.start('https://<page3>');
+
+  // Then we have a page that we know need to wait longer, start measuring
+  await command.measure.start('MySpecialPage');
+  // Go to the page
+  await commands.navigate('https://<myspecialpage>');
+  // Then you need to wait on a specific element or event. In this case
+  // we wait for a id to appear but you could also run your custom JS
+  await commands.wait.byId('my-id', 20000);
+  // And then when you know that page has loaded stop the measurement
+  // = stop the video, collect metrics etc
+  return commands.measure.stop();
+};
+

Test the same page multiple times within the same run

If you for some reason want to test the same URL within the same run multiple times, it will not work out of the box since the current version create the result files using the URL. For example testing https://www.sitespeed.io/ two times, will break since the second access will try to overwrite the first one.

But there is a hack you can do. If you add a dummy query parameter (and give the page an alias) you can test them twice.

/**
+ * @param {import('browsertime').BrowsertimeContext} context
+ * @param {import('browsertime').BrowsertimeCommands} commands
+ */
+export default async function (context, commands) {
+    await commands.measure.start('https://www.sitespeed.io/', 'HomePage');
+
+    // Do something smart that then make you need to test the same URL again
+    // ...
+
+    return commands.navigate('https://www.sitespeed.io/?dummy', 'BackToHomepage');
+};
+

Using setUp and tearDown in the same script

This is a feature used by Mozilla and was created years ago. Nowadays you can probably just do everything in one script.

Scripts can also directly define the --preScript and --postScript options by implementing a setUp and/or a tearDown function. These functions will get the same arguments than the test itself. When using this form, the three functions are declared in module.exports under the setUp, tearDown and test keys. This works for commons JS files.

Here's a minimal example:

async function setUp(context, commands) {
+  // do some useful set up
+};
+
+async function perfTest(context, commands) {
+  // add your own code here
+};
+
+async function tearDown(context, commands) {
+  // do some cleanup here
+};
+
+module.exports = {
+  setUp: setUp,
+  tearDown: tearDown,
+  test: perfTest
+};
+
\ No newline at end of file