test_restore_from_backup.html (20149B)
1 <!DOCTYPE HTML> 2 <html> 3 <head> 4 <meta charset="utf-8"> 5 <title>Tests for the restore-from-backup component</title> 6 <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> 7 <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> 8 <script type="application/javascript" src="head.js"></script> 9 <script 10 src="chrome://browser/content/backup/restore-from-backup.mjs" 11 type="module" 12 ></script> 13 <link rel="localization" href="browser/backupSettings.ftl"/> 14 <link rel="localization" href="branding/brand.ftl"/> 15 <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> 16 <script> 17 18 const { BrowserTestUtils } = ChromeUtils.importESModule( 19 "resource://testing-common/BrowserTestUtils.sys.mjs" 20 ); 21 const { ERRORS } = ChromeUtils.importESModule( 22 "chrome://browser/content/backup/backup-constants.mjs" 23 ); 24 25 /** 26 * Tests that adding a restore-from-backup element to the DOM causes it to 27 * fire a BackupUI:InitWidget event. 28 */ 29 add_task(async function test_initWidget() { 30 let restoreFromBackup = document.createElement("restore-from-backup"); 31 let content = document.getElementById("content"); 32 33 let sawInitWidget = BrowserTestUtils.waitForEvent(content, "BackupUI:InitWidget"); 34 content.appendChild(restoreFromBackup); 35 await sawInitWidget; 36 ok(true, "Saw BackupUI:InitWidget"); 37 38 restoreFromBackup.remove(); 39 }); 40 41 /** 42 * Tests that pressing the restore and restart button will dispatch the expected events. 43 */ 44 add_task(async function test_restore() { 45 let restoreFromBackup = document.getElementById("test-restore-from-backup"); 46 let confirmButton = restoreFromBackup.confirmButtonEl; 47 48 ok(confirmButton, "Restore button should be found"); 49 50 restoreFromBackup.backupServiceState = { 51 ...restoreFromBackup.backupServiceState, 52 backupFileToRestore: "/Some/User/Documents/Firefox Backup/backup.html" 53 }; 54 await restoreFromBackup.updateComplete; 55 56 let content = document.getElementById("content"); 57 let promise = BrowserTestUtils.waitForEvent(content, "BackupUI:RestoreFromBackupFile"); 58 59 confirmButton.click(); 60 61 await promise; 62 63 ok(true, "Detected event after pressing the restore button"); 64 }); 65 66 /** 67 * Tests that pressing the cancel button will dispatch the expected events. 68 */ 69 add_task(async function test_cancel() { 70 let restoreFromBackup = document.getElementById("test-restore-from-backup"); 71 let cancelButton = restoreFromBackup.cancelButtonEl; 72 73 ok(cancelButton, "Cancel button should be found"); 74 75 let content = document.getElementById("content"); 76 let promise = BrowserTestUtils.waitForEvent(content, "dialogCancel"); 77 78 cancelButton.click(); 79 80 await promise; 81 ok(true, "Detected event after pressing the cancel button"); 82 }); 83 84 /** 85 * Tests that pressing the choose button will dispatch the expected events. 86 */ 87 add_task(async function test_choose() { 88 let restoreFromBackup = document.getElementById("test-restore-from-backup"); 89 restoreFromBackup.backupServiceState.backupFileToRestore = "/backup/file\\path.html"; 90 91 ok(restoreFromBackup.chooseButtonEl, "Choose button should be found"); 92 93 let content = document.getElementById("content"); 94 let promise = BrowserTestUtils.waitForEvent(content, "BackupUI:ShowFilepicker"); 95 96 restoreFromBackup.chooseButtonEl.click(); 97 98 let event = await promise; 99 ok(true, "Detected event after pressing the choose button"); 100 is(event.detail.win, window.browsingContext, "Current window will be the parent"); 101 is(event.detail.filter, "filterHTML", "Only HTML files will be shown"); 102 is(event.detail.existingBackupPath, "/backup/file\\path.html", "Path to the backup file is given"); 103 }); 104 105 /** 106 * Tests that selecting a backup file from the filepicker will dispatch the expected events. 107 */ 108 add_task(async function test_new_file_selected() { 109 let restoreFromBackup = document.getElementById("test-restore-from-backup"); 110 111 let content = document.getElementById("content"); 112 let promise = BrowserTestUtils.waitForEvent(content, "BackupUI:GetBackupFileInfo"); 113 114 let selectEvent = new CustomEvent("BackupUI:SelectNewFilepickerPath", { 115 detail: { 116 path: "/Some/User/Documents/Firefox Backup/backup-default.html", 117 iconURL: "chrome://global/skin/icons/document.svg" 118 } 119 }); 120 restoreFromBackup.dispatchEvent(selectEvent); 121 122 await promise; 123 ok(true, "Detected event after new file is selected"); 124 }); 125 126 /** 127 * Tests that the password input will shown when a file is encrypted. 128 */ 129 add_task(async function test_show_password() { 130 let restoreFromBackup = document.getElementById("test-restore-from-backup"); 131 132 ok(!restoreFromBackup.passwordInput, "Password input should not be present"); 133 134 let date = new Date(); 135 restoreFromBackup.backupServiceState = { 136 ...restoreFromBackup.backupServiceState, 137 backupFileInfo: { 138 date, 139 isEncrypted: true, 140 } 141 }; 142 143 await restoreFromBackup.updateComplete; 144 145 ok(restoreFromBackup.passwordInput, "Password input should be present"); 146 }); 147 148 /** 149 * Tests that incorrect password is shown in a different error format than top level 150 */ 151 add_task(async function test_incorrect_password_error_condition() { 152 let restoreFromBackup = document.getElementById("test-restore-from-backup"); 153 154 is(restoreFromBackup.backupServiceState.recoveryErrorCode, ERRORS.NONE, "Recovery error code should be 0"); 155 ok(!restoreFromBackup.isIncorrectPassword, "Error message should not be displayed"); 156 ok(!restoreFromBackup.errorMessageEl, "No error message should be displayed"); 157 158 restoreFromBackup.backupServiceState = { 159 ...restoreFromBackup.backupServiceState, 160 recoveryErrorCode: ERRORS.UNAUTHORIZED 161 }; 162 163 await restoreFromBackup.updateComplete; 164 165 is(restoreFromBackup.backupServiceState.recoveryErrorCode, ERRORS.UNAUTHORIZED, "Recovery error code should be set"); 166 ok(restoreFromBackup.isIncorrectPassword, "Error message should be displayed"); 167 ok(!restoreFromBackup.errorMessageEl, "No top level error message should be displayed"); 168 }); 169 170 /** 171 * Tests that a top level error message is displayed if there is an error restoring from backup. 172 */ 173 add_task(async function test_error_condition() { 174 let restoreFromBackup = document.getElementById("test-restore-from-backup"); 175 176 ok(!restoreFromBackup.errorMessageEl, "No error message should be displayed"); 177 178 restoreFromBackup.backupServiceState = { 179 ...restoreFromBackup.backupServiceState, 180 recoveryErrorCode: ERRORS.CORRUPTED_ARCHIVE 181 }; 182 183 await restoreFromBackup.updateComplete; 184 185 is(restoreFromBackup.backupServiceState.recoveryErrorCode, ERRORS.CORRUPTED_ARCHIVE, "Recovery error code should be set"); 186 ok(restoreFromBackup.errorMessageEl, "Error message should be displayed"); 187 }); 188 189 /** 190 * Tests that changes to backupServiceState emits BackupUI:RecoveryProgress, 191 * with the current progress state and progress is false when an error code is present. 192 */ 193 add_task(async function test_recovery_state_updates() { 194 const content = document.getElementById("content"); 195 let restoreFromBackup = document.getElementById("test-restore-from-backup"); 196 content.appendChild(restoreFromBackup); 197 198 // Reset previous state changes 199 restoreFromBackup.backupServiceState = { 200 ...restoreFromBackup.backupServiceState, 201 recoveryInProgress: false, 202 recoveryErrorCode: ERRORS.NONE, 203 }; 204 await restoreFromBackup.updateComplete; 205 206 // Helper to dispatch the BackupUI:RecoveryProgress event 207 async function sendState(testState) { 208 const promise = BrowserTestUtils.waitForEvent( 209 restoreFromBackup, 210 "BackupUI:RecoveryProgress" 211 ); 212 restoreFromBackup.backupServiceState = { 213 ...restoreFromBackup.backupServiceState, 214 ...testState, 215 }; 216 await restoreFromBackup.updateComplete; 217 return promise; 218 } 219 220 // Initial state 221 is( 222 restoreFromBackup.backupServiceState.recoveryInProgress, 223 false, 224 "Initial progress state is false" 225 ); 226 is(restoreFromBackup.backupServiceState.recoveryErrorCode, ERRORS.NONE, "Initial error code should be 0"); 227 228 // Backup in progress with no error 229 let event = await sendState({ recoveryInProgress: true }); 230 is(event.detail?.recoveryInProgress, true, "'recoveryInProgress' is true"); 231 is(restoreFromBackup.backupServiceState.recoveryInProgress, true, "State reflects in-progress"); 232 233 // Backup not in progress 234 event = await sendState({ recoveryInProgress: false, recoveryErrorCode: 0 }); 235 is(event.detail?.recoveryInProgress, false, "'recoveryInProgress' is false"); 236 is( 237 restoreFromBackup.backupServiceState.recoveryInProgress, 238 false, 239 "State reflects not in-progress" 240 ); 241 242 // Any error should clear progress 243 for (const code of [ERRORS.CORRUPTED_ARCHIVE, ERRORS.UNAUTHORIZED]) { 244 info(`Asserting recovery progress clears with error code: ${code}`); 245 event = await sendState({ 246 recoveryInProgress: true, 247 recoveryErrorCode: ERRORS.NONE, 248 }); 249 is( 250 restoreFromBackup.backupServiceState.recoveryInProgress, 251 true, 252 "State reflects in-progress" 253 ); 254 255 // Add an error 256 event = await sendState({ 257 recoveryInProgress: true, 258 recoveryErrorCode: code, 259 }); 260 is( 261 event.detail?.recoveryInProgress, 262 false, 263 `Progress cleared for error ${code}` 264 ); 265 // Clear state 266 await sendState({ recoveryInProgress: false, recoveryErrorCode: ERRORS.NONE }); 267 } 268 restoreFromBackup.remove(); 269 }); 270 271 /** 272 * Helper function to test that a support link has correct attributes 273 * and UTM params when used with aboutWelcomeEmbedded 274 * 275 * @param {Element} link - The support link element to test 276 * @param {string} linkName - The name of the link to test 277 */ 278 279 function assertEmbeddedSupportLink(link, linkName) { 280 ok(link, `${linkName} should be present`); 281 ok( 282 !link.hasAttribute("is"), 283 `${linkName} should not have 'is' attribute` 284 ); 285 ok( 286 !link.hasAttribute("support-page"), 287 `${linkName} should not have support-page attribute` 288 ); 289 ok( 290 link.hasAttribute("href"), 291 `${linkName} should have href attribute` 292 ); 293 294 let url = new URL(link.getAttribute("href")); 295 296 is( 297 url.searchParams.get("utm_medium"), 298 "firefox-desktop", 299 `${linkName} should have correct utm_medium` 300 ); 301 is( 302 url.searchParams.get("utm_source"), 303 "npo", 304 `${linkName} should have correct utm_source` 305 ); 306 is( 307 url.searchParams.get("utm_campaign"), 308 "fx-backup-restore", 309 `${linkName} should have correct utm_campaign` 310 ); 311 is( 312 url.searchParams.get("utm_content"), 313 "restore-error", 314 `${linkName} should have correct utm_content` 315 ); 316 is( 317 link.getAttribute("target"), 318 "_blank", 319 `${linkName} should have target='_blank'` 320 ); 321 } 322 323 /** 324 * Tests that support links have UTM parameters when aboutWelcomeEmbedded is true 325 */ 326 add_task(async function test_support_links_with_utm_params() { 327 let content = document.getElementById("content"); 328 let restoreFromBackup = document.createElement("restore-from-backup"); 329 content.appendChild(restoreFromBackup); 330 331 // Set up the support base link for testing, otherwise links will be broken 332 restoreFromBackup.backupServiceState = { 333 ...restoreFromBackup.backupServiceState, 334 supportBaseLink: "https://support.mozilla.org/", 335 }; 336 restoreFromBackup.aboutWelcomeEmbedded = true; 337 await restoreFromBackup.updateComplete; 338 339 // Test the "no backup file" link 340 let noBackupFileLink = restoreFromBackup.shadowRoot.querySelector( 341 "#restore-from-backup-no-backup-file-link" 342 ); 343 assertEmbeddedSupportLink(noBackupFileLink, "No backup file link"); 344 345 // Test the incorrect password support link 346 restoreFromBackup.backupServiceState = { 347 ...restoreFromBackup.backupServiceState, 348 backupFileInfo: { 349 date: new Date(), 350 isEncrypted: true, 351 }, 352 recoveryErrorCode: ERRORS.UNAUTHORIZED, 353 }; 354 await restoreFromBackup.updateComplete; 355 let passwordErrorLink = restoreFromBackup.shadowRoot.querySelector( 356 "#backup-incorrect-password-support-link" 357 ); 358 assertEmbeddedSupportLink(passwordErrorLink, "Password error link"); 359 360 restoreFromBackup.remove(); 361 }); 362 363 /** 364 * Tests that the correct status is displayed under the input 365 * for different backup file info states. 366 */ 367 add_task(async function test_backup_file_status_rendering() { 368 let content = document.getElementById("content"); 369 let restoreFromBackup = document.createElement("restore-from-backup"); 370 content.appendChild(restoreFromBackup); 371 372 // Test that when no backup file is selected, the support link is displayed 373 restoreFromBackup.backupServiceState = { 374 ...restoreFromBackup.backupServiceState, 375 backupFileInfo: null, 376 recoveryErrorCode: ERRORS.NONE, 377 }; 378 await restoreFromBackup.updateComplete; 379 380 let noBackupLink = restoreFromBackup.shadowRoot.querySelector( 381 "#restore-from-backup-no-backup-file-link" 382 ); 383 ok(noBackupLink, "Should show support link when no backup file is selected"); 384 385 let backupInfo = restoreFromBackup.shadowRoot.querySelector( 386 "#restore-from-backup-backup-found-info" 387 ); 388 ok(!backupInfo, "Should not show backup info when no backup file is selected"); 389 390 // Test that when a backup file is selected, the backup info is displayed 391 const IS_ENCRYPTED = true; 392 const DATE = new Date("2024-01-01T00:00:00.000Z"); 393 const DEVICE_NAME = "test-device"; 394 395 restoreFromBackup.backupServiceState = { 396 ...restoreFromBackup.backupServiceState, 397 backupFileInfo: { 398 isEncrypted: IS_ENCRYPTED, 399 date: DATE, 400 deviceName: DEVICE_NAME 401 }, 402 recoveryErrorCode: ERRORS.NONE, 403 }; 404 await restoreFromBackup.updateComplete; 405 406 noBackupLink = restoreFromBackup.shadowRoot.querySelector( 407 "#restore-from-backup-no-backup-file-link" 408 ); 409 ok(!noBackupLink, "Should not show support link when backup file is found"); 410 411 backupInfo = restoreFromBackup.shadowRoot.querySelector( 412 "#restore-from-backup-backup-found-info" 413 ); 414 ok(backupInfo, "Should show backup info when backup file is found"); 415 is(backupInfo.getAttribute("data-l10n-id"), "backup-file-creation-date-and-device", 416 "Should have correct l10n id for backup info"); 417 418 // Test that when embedded in about:welcome, if an error occurs, 419 // the generic file error template is displayed 420 restoreFromBackup.aboutWelcomeEmbedded = true; 421 restoreFromBackup.backupServiceState = { 422 ...restoreFromBackup.backupServiceState, 423 backupFileInfo: null, 424 recoveryErrorCode: ERRORS.CORRUPTED_ARCHIVE, 425 }; 426 await restoreFromBackup.updateComplete; 427 428 let errorTemplate = restoreFromBackup.shadowRoot.querySelector( 429 "#backup-generic-file-error" 430 ); 431 ok(errorTemplate, "Should show error template in embedded context with error"); 432 433 // Test that when not embedded in about:welcome, if an error occurs, 434 // the support link is displayed instead of the generic file error template 435 restoreFromBackup.aboutWelcomeEmbedded = false; 436 restoreFromBackup.backupServiceState = { 437 ...restoreFromBackup.backupServiceState, 438 backupFileInfo: null, 439 recoveryErrorCode: ERRORS.FILE_SYSTEM_ERROR, 440 }; 441 await restoreFromBackup.updateComplete; 442 443 errorTemplate = restoreFromBackup.shadowRoot.querySelector( 444 "#backup-generic-file-error" 445 ); 446 ok(!errorTemplate, "Should not show generic file error template when not embedded in about:welcome"); 447 448 noBackupLink = restoreFromBackup.shadowRoot.querySelector( 449 "#restore-from-backup-no-backup-file-link" 450 ); 451 ok(noBackupLink, "Should show support link when not embedded in about:welcome, with error"); 452 453 restoreFromBackup.remove(); 454 }); 455 456 /** 457 * Tests that when embedded in about:welcome, the textarea's aria-describedby 458 * attribute correctly references the appropriate element based on the displayed status. 459 */ 460 add_task(async function test_textarea_aria_describedby_accessibility() { 461 let content = document.getElementById("content"); 462 let restoreFromBackup = document.createElement("restore-from-backup"); 463 content.appendChild(restoreFromBackup); 464 465 restoreFromBackup.aboutWelcomeEmbedded = true; 466 await restoreFromBackup.updateComplete; 467 468 let textarea = restoreFromBackup.shadowRoot.querySelector("#backup-filepicker-input"); 469 ok(textarea, "Textarea should be present when aboutWelcomeEmbedded is true"); 470 471 // Test that when there is no backup file info, we should reference no-backup-file-link 472 restoreFromBackup.backupServiceState = { 473 ...restoreFromBackup.backupServiceState, 474 backupFileInfo: null, 475 recoveryErrorCode: ERRORS.NONE, 476 }; 477 await restoreFromBackup.updateComplete; 478 479 let ariaDescribedBy = textarea.getAttribute("aria-describedby"); 480 is(ariaDescribedBy, "restore-from-backup-no-backup-file-link", 481 "aria-describedby should reference no-backup-file-link when no backup info"); 482 483 let referencedElement = restoreFromBackup.shadowRoot.querySelector("#restore-from-backup-no-backup-file-link"); 484 ok(referencedElement, "Referenced element should exist"); 485 486 // Test that when a backup file is found, we should reference backup-found-info 487 restoreFromBackup.backupServiceState = { 488 ...restoreFromBackup.backupServiceState, 489 backupFileInfo: { 490 date: new Date(), 491 deviceName: "test-device", 492 isEncrypted: false, 493 }, 494 recoveryErrorCode: ERRORS.NONE, 495 }; 496 await restoreFromBackup.updateComplete; 497 498 ariaDescribedBy = textarea.getAttribute("aria-describedby"); 499 is(ariaDescribedBy, "restore-from-backup-backup-found-info", 500 "aria-describedby should reference backup-found-info when backup file is found"); 501 502 referencedElement = restoreFromBackup.shadowRoot.querySelector("#restore-from-backup-backup-found-info"); 503 ok(referencedElement, "Referenced element should exist"); 504 505 // Test that when we're in an error state, we should reference the generic-file-error 506 restoreFromBackup.backupServiceState = { 507 ...restoreFromBackup.backupServiceState, 508 backupFileInfo: null, 509 recoveryErrorCode: ERRORS.CORRUPTED_ARCHIVE, 510 }; 511 await restoreFromBackup.updateComplete; 512 513 ariaDescribedBy = textarea.getAttribute("aria-describedby"); 514 is(ariaDescribedBy, "backup-generic-file-error", 515 "aria-describedby should reference generic-file-error when there's a recovery error"); 516 517 referencedElement = restoreFromBackup.shadowRoot.querySelector("#backup-generic-file-error"); 518 ok(referencedElement, "Referenced element should exist"); 519 520 restoreFromBackup.remove(); 521 }); 522 </script> 523 </head> 524 <body> 525 <p id="display"></p> 526 <div id="content" style="display: none"> 527 <restore-from-backup id="test-restore-from-backup"></restore-from-backup> 528 </div> 529 <pre id="test"></pre> 530 </body> 531 </html>