head.js (9708B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 const { NavigationManager } = ChromeUtils.importESModule( 8 "chrome://remote/content/shared/NavigationManager.sys.mjs" 9 ); 10 11 const numberRegex = /[0-9]+/i; 12 const uuidRegex = 13 /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; 14 15 /** 16 * Add a new tab in a given browser, pointing to a given URL and automatically 17 * register the cleanup function to remove it at the end of the test. 18 * 19 * @param {Browser} browser 20 * The browser element where the tab should be added. 21 * @param {string} url 22 * The URL for the tab. 23 * @param {object=} options 24 * Options object to forward to BrowserTestUtils.addTab. 25 * @returns {Tab} 26 * The created tab. 27 */ 28 function addTab(browser, url, options) { 29 info("Add a new tab for url: " + url); 30 const tab = BrowserTestUtils.addTab(browser, url, options); 31 registerCleanupFunction(() => browser.removeTab(tab)); 32 return tab; 33 } 34 35 /** 36 * Add a new tab and wait until the navigation manager emitted the corresponding 37 * navigation-stopped event. Same arguments as addTab. 38 * 39 * @param {Browser} browser 40 * The browser element where the tab should be added. 41 * @param {string} url 42 * The URL for the tab. 43 * @param {object=} options 44 * Options object to forward to BrowserTestUtils.addTab. 45 * @returns {Tab} 46 * The created tab. 47 */ 48 async function addTabAndWaitForNavigated(browser, url, options) { 49 // Setup navigation manager and promises. 50 const { promise: waitForNavigation, resolve } = Promise.withResolvers(); 51 const onNavigationStopped = (name, data) => { 52 if (data?.url === url) { 53 resolve(); 54 } 55 }; 56 const navigationManager = new NavigationManager(); 57 navigationManager.on("navigation-stopped", onNavigationStopped); 58 navigationManager.startMonitoring(); 59 60 // Add the new tab. 61 const tab = addTab(browser, url, options); 62 63 // See Bug 1971329, browserLoaded on its own might still miss the STATE_STOP 64 // notification. 65 info("Wait for BrowserTestUtils.browserLoaded for url: " + url); 66 await BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, url); 67 68 info("Wait for navigation-stopped for url: " + url); 69 await waitForNavigation; 70 71 // Wait for tick to allow other callbacks listening for STATE_STOP to be 72 // processed correctly. 73 await TestUtils.waitForTick(); 74 75 navigationManager.stopMonitoring(); 76 navigationManager.off("navigation-stopped", onNavigationStopped); 77 navigationManager.destroy(); 78 79 return tab; 80 } 81 82 /** 83 * Check if a given navigation is valid and has the expected url. 84 * 85 * @param {object} navigation 86 * The navigation to validate. 87 * @param {string} expectedUrl 88 * The expected url for the navigation. 89 */ 90 function assertNavigation(navigation, expectedUrl) { 91 ok(!!navigation, "Retrieved a navigation"); 92 is(navigation.url, expectedUrl, "Navigation has the expected URL"); 93 is( 94 typeof navigation.navigationId, 95 "string", 96 "Navigation has a string navigationId" 97 ); 98 } 99 100 /** 101 * Check a pair of navigation events have the expected URL, navigation id and 102 * navigable id. The pair is expected to be ordered as follows: navigation-started 103 * and then navigation-stopped. 104 * 105 * @param {Array<object>} events 106 * The pair of events to validate. 107 * @param {string} url 108 * The expected url for the navigation. 109 * @param {string} navigationId 110 * The expected navigation id. 111 * @param {string} navigableId 112 * The expected navigable id. 113 * @param {boolean} isSameDocument 114 * If the navigation should be a same document navigation. 115 */ 116 function assertNavigationEvents( 117 events, 118 url, 119 navigationId, 120 navigableId, 121 isSameDocument 122 ) { 123 const expectedEvents = isSameDocument ? 1 : 2; 124 125 const navigationEvents = events.filter( 126 e => e.data.navigationId == navigationId 127 ); 128 is( 129 navigationEvents.length, 130 expectedEvents, 131 `Found ${expectedEvents} events for navigationId ${navigationId}` 132 ); 133 134 const sameDocumentEvents = ["fragment-navigated", "same-document-changed"]; 135 136 if (isSameDocument) { 137 // Check there are no navigation-started/stopped events. 138 ok(!navigationEvents.some(e => e.name === "navigation-started")); 139 ok(!navigationEvents.some(e => e.name === "navigation-stopped")); 140 141 const locationChanged = navigationEvents.find(e => 142 sameDocumentEvents.includes(e.name) 143 ); 144 ok( 145 sameDocumentEvents.includes(locationChanged.name), 146 "event has the expected name" 147 ); 148 is(locationChanged.data.url, url, "event has the expected url"); 149 is( 150 locationChanged.data.navigableId, 151 navigableId, 152 "event has the expected navigable" 153 ); 154 } else { 155 // Check there is no fragment-navigated/same-document-changed event. 156 ok(!navigationEvents.some(e => sameDocumentEvents.includes(e.name))); 157 158 const started = navigationEvents.find(e => e.name === "navigation-started"); 159 const stopped = navigationEvents.find(e => e.name === "navigation-stopped"); 160 161 // Check navigation-started 162 is(started.name, "navigation-started", "event has the expected name"); 163 is(started.data.url, url, "event has the expected url"); 164 is( 165 started.data.navigableId, 166 navigableId, 167 "event has the expected navigable" 168 ); 169 170 // Check navigation-stopped 171 is(stopped.name, "navigation-stopped", "event has the expected name"); 172 is(stopped.data.url, url, "event has the expected url"); 173 is( 174 stopped.data.navigableId, 175 navigableId, 176 "event has the expected navigable" 177 ); 178 } 179 } 180 181 /** 182 * Assert that the given navigations all have unique/different ids. 183 * 184 * @param {Array<object>} navigations 185 * The navigations to validate. 186 */ 187 function assertUniqueNavigationIds(...navigations) { 188 const ids = navigations.map(navigation => navigation.navigationId); 189 is(new Set(ids).size, ids.length, "Navigation ids are all different"); 190 } 191 192 /** 193 * Create a document-builder based page with an iframe served by a given domain. 194 * 195 * @param {string} domain 196 * The domain which should serve the page. 197 * @returns {string} 198 * The URI for the page. 199 */ 200 function createFrame(domain) { 201 return createFrameForUri( 202 `https://${domain}/document-builder.sjs?html=frame-${domain}` 203 ); 204 } 205 206 /** 207 * Create the markup for an iframe pointing to a given URI. 208 * 209 * @param {string} uri 210 * The uri for the iframe. 211 * @returns {string} 212 * The iframe markup. 213 */ 214 function createFrameForUri(uri) { 215 return `<iframe src="${encodeURI(uri)}"></iframe>`; 216 } 217 218 /** 219 * Create the URL for a test page containing nested iframes 220 * 221 * @returns {string} 222 * The test page url. 223 */ 224 function createTestPageWithFrames() { 225 // Create the markup for an example.net frame nested in an example.com frame. 226 const NESTED_FRAME_MARKUP = createFrameForUri( 227 `https://example.org/document-builder.sjs?html=${createFrame( 228 "example.net" 229 )}` 230 ); 231 232 // Combine the nested frame markup created above with an example.com frame. 233 const TEST_URI_MARKUP = `${NESTED_FRAME_MARKUP}${createFrame("example.com")}`; 234 235 // Create the test page URI on example.org. 236 return `https://example.org/document-builder.sjs?html=${encodeURI( 237 TEST_URI_MARKUP 238 )}`; 239 } 240 241 /** 242 * Load the provided url in an existing browser. 243 * 244 * @param {Browser} browser 245 * The browser element where the URL should be loaded. 246 * @param {string} url 247 * The URL to load. 248 * @param {object=} options 249 * @param {boolean} options.includeSubFrames 250 * Whether we should monitor load of sub frames. Defaults to false. 251 * @param {boolean} options.maybeErrorPage 252 * Whether we might reach an error page or not. Defaults to false. 253 * @returns {Promise} 254 * Promise which will resolve when the page is loaded with the expected url. 255 */ 256 async function loadURL(browser, url, options = {}) { 257 const { includeSubFrames = false, maybeErrorPage = false } = options; 258 const loaded = BrowserTestUtils.browserLoaded( 259 browser, 260 includeSubFrames, 261 url, 262 maybeErrorPage 263 ); 264 BrowserTestUtils.startLoadingURIString(browser, url); 265 return loaded; 266 } 267 268 /** 269 * For a support file resolve its relative path to the absolute path. 270 * 271 * @param {string} path 272 * The path or a filename of a support file. 273 * @returns {string} 274 * Absolute path of the support file. 275 */ 276 function getSupportFilePath(path) { 277 let absolutePath = getChromeDir(getResolvedURI(gTestPath)); 278 279 for (const part of path.split("/")) { 280 if (part === "..") { 281 absolutePath = absolutePath.parent; 282 } else { 283 absolutePath.append(part); 284 } 285 } 286 287 if (!absolutePath.exists()) { 288 throw new Error(`${absolutePath.path} does not exist`); 289 } 290 291 return absolutePath.path; 292 } 293 294 /** 295 * Reads file from provided path and returns its contents encoded with base64 296 * 297 * @param {string} path 298 * The Path to load. 299 * @returns {Promise} 300 * Promise which will resolved when the file finished loading. 301 */ 302 async function readFileAsBase64(path) { 303 const file = new FileUtils.File(path); 304 305 const contents = await new Promise((resolve, reject) => { 306 NetUtil.asyncFetch( 307 { 308 uri: file, 309 loadUsingSystemPrincipal: true, 310 }, 311 (inputStream, status) => { 312 if (!Components.isSuccessCode(status)) { 313 reject(new Error("Failed to read file; status = " + status)); 314 return; 315 } 316 317 const fileContents = NetUtil.readInputStreamToString( 318 inputStream, 319 inputStream.available() 320 ); 321 inputStream.close(); 322 323 resolve(fileContents); 324 } 325 ); 326 }); 327 328 return btoa(contents); 329 }