test_taskbarTabs_pin.js (10811B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 ChromeUtils.defineESModuleGetters(this, { 7 FileTestUtils: "resource://testing-common/FileTestUtils.sys.mjs", 8 MockRegistrar: "resource://testing-common/MockRegistrar.sys.mjs", 9 sinon: "resource://testing-common/Sinon.sys.mjs", 10 ShellService: "moz-src:///browser/components/shell/ShellService.sys.mjs", 11 TaskbarTabsPin: "resource:///modules/taskbartabs/TaskbarTabsPin.sys.mjs", 12 TaskbarTabsRegistry: 13 "resource:///modules/taskbartabs/TaskbarTabsRegistry.sys.mjs", 14 XPCOMUtils: "resource://gre/modules/XPCOMUtils.sys.mjs", 15 }); 16 17 XPCOMUtils.defineLazyServiceGetters(this, { 18 imgTools: ["@mozilla.org/image/tools;1", Ci.imgITools], 19 }); 20 21 // We want to mock the native XPCOM interfaces of the initialized 22 // `ShellService.shellService`, but those interfaces are frozen. Instead we 23 // proxy `ShellService.shellService` and mock it. 24 let gCreateWindowsIcon = ShellService.shellService.createWindowsIcon; 25 let gOverrideWindowsIconFileOnce; 26 const kMockNativeShellService = { 27 ...ShellService.shellService, 28 createWindowsIcon: sinon 29 .stub() 30 .callsFake(async (aIconFile, aImgContainer) => { 31 if (gOverrideWindowsIconFileOnce) { 32 await gCreateWindowsIcon(gOverrideWindowsIconFileOnce, aImgContainer); 33 gOverrideWindowsIconFileOnce = null; 34 } 35 }), 36 createShortcut: sinon.stub().resolves("dummy_path"), 37 deleteShortcut: sinon.stub().resolves("dummy_path"), 38 pinShortcutToTaskbar: sinon.stub().resolves(), 39 getTaskbarTabShortcutPath: sinon 40 .stub() 41 .returns(FileTestUtils.getTempFile().parent.path), 42 unpinShortcutFromTaskbar: sinon.stub(), 43 }; 44 45 sinon.stub(ShellService, "shellService").value(kMockNativeShellService); 46 47 sinon.stub(TaskbarTabsPin, "_getLocalization").returns({ 48 formatValue(msg) { 49 // Slash must also be sanitized, so it should appear as '_' in paths. 50 return `[formatValue/${msg}]`; 51 }, 52 }); 53 54 registerCleanupFunction(() => { 55 sinon.restore(); 56 }); 57 58 // Favicons are written to the profile directory, ensure it exists. 59 do_get_profile(); 60 61 const kFaviconService = Cc[ 62 "@mozilla.org/browser/favicon-service;1" 63 ].createInstance(Ci.nsIFaviconService); 64 65 let gPngFavicon; 66 let gSvgFavicon; 67 add_setup(async () => { 68 const pngFile = do_get_file("favicon-normal16.png"); 69 const pngData = await IOUtils.read(pngFile.path); 70 gPngFavicon = { rawData: pngData.buffer, mimeType: "image/png" }; 71 72 const svgFile = do_get_file("icon.svg"); 73 gSvgFavicon = { 74 dataURI: Services.io.newFileURI(svgFile), 75 mimeType: "image/svg+xml", 76 }; 77 }); 78 79 let gFavicon; 80 81 const kMockFaviconService = { 82 QueryInterface: ChromeUtils.generateQI(["nsIFaviconService"]), 83 getFaviconForPage: sinon.stub().callsFake(async () => { 84 return gFavicon; 85 }), 86 get defaultFavicon() { 87 return kFaviconService.defaultFavicon; 88 }, 89 }; 90 const kDefaultIconSpy = sinon.spy(kMockFaviconService, "defaultFavicon", [ 91 "get", 92 ]); 93 94 function shellPinCalled(aTaskbarTab) { 95 ok( 96 kMockNativeShellService.createWindowsIcon.calledOnce, 97 `Icon creation should have been called.` 98 ); 99 ok( 100 kMockNativeShellService.createShortcut.calledOnce, 101 `Shortcut creation should have been called.` 102 ); 103 ok( 104 kMockNativeShellService.pinShortcutToTaskbar.calledOnce, 105 `Pin to taskbar should have been called.` 106 ); 107 Assert.equal( 108 kMockNativeShellService.pinShortcutToTaskbar.firstCall.args[1], 109 kMockNativeShellService.createShortcut.firstCall.args[6], 110 `The created and pinned shortcuts should be in the same folder.` 111 ); 112 Assert.equal( 113 kMockNativeShellService.pinShortcutToTaskbar.firstCall.args[2], 114 kMockNativeShellService.createShortcut.firstCall.args[7], 115 `The created and pinned shortcuts should be the same file.` 116 ); 117 Assert.equal( 118 kMockNativeShellService.pinShortcutToTaskbar.firstCall.args[2], 119 aTaskbarTab.shortcutRelativePath, 120 `The pinned shortcut should be the saved shortcut.` 121 ); 122 } 123 124 function shellUnpinCalled() { 125 ok( 126 kMockNativeShellService.deleteShortcut.calledOnce, 127 `Unpin from taskbar should have been called.` 128 ); 129 ok( 130 kMockNativeShellService.unpinShortcutFromTaskbar.calledOnce, 131 `Unpin from taskbar should have been called.` 132 ); 133 } 134 135 MockRegistrar.register( 136 "@mozilla.org/browser/favicon-service;1", 137 kMockFaviconService 138 ); 139 140 async function testWrittenIconFile(aIconFile) { 141 const data = await IOUtils.read(aIconFile.path); 142 const imgContainer = imgTools.decodeImageFromArrayBuffer( 143 data.buffer, 144 "image/vnd.microsoft.icon" 145 ); 146 equal( 147 imgContainer.width, 148 256, 149 "Image written to disk should be 256px width." 150 ); 151 equal( 152 imgContainer.height, 153 256, 154 "Image written to disk should be 256px height." 155 ); 156 } 157 158 const url = Services.io.newURI("https://www.test.com"); 159 const userContextId = 0; 160 161 const registry = new TaskbarTabsRegistry(); 162 const taskbarTab = createTaskbarTab(registry, url, userContextId); 163 164 const patchedSpy = sinon.stub(); 165 registry.on(TaskbarTabsRegistry.events.patched, patchedSpy); 166 167 function getTempFile() { 168 let path = do_get_tempdir(); 169 let filename = Services.uuid.generateUUID().toString().slice(1, -1); 170 path.append(filename + ".ico"); 171 return path; 172 } 173 174 add_task(async function test_pin_existing_favicon_raster() { 175 sinon.resetHistory(); 176 gFavicon = gPngFavicon; 177 178 let iconFile = getTempFile(); 179 gOverrideWindowsIconFileOnce = iconFile; 180 181 await TaskbarTabsPin.pinTaskbarTab(taskbarTab, registry); 182 183 ok( 184 kMockFaviconService.getFaviconForPage.calledOnce, 185 "The favicon for the page should have attempted to be retrieved." 186 ); 187 const imgContainer = 188 kMockNativeShellService.createWindowsIcon.firstCall.args[1]; 189 equal(imgContainer.width, 256, "Image should be scaled to 256px width."); 190 equal(imgContainer.height, 256, "Image should be scaled to 256px height."); 191 ok( 192 kDefaultIconSpy.get.notCalled, 193 "The default icon should not be used when a favicon exists for the page." 194 ); 195 196 await testWrittenIconFile(iconFile); 197 198 shellPinCalled(taskbarTab); 199 }); 200 201 add_task(async function test_pin_existing_favicon_vector() { 202 sinon.resetHistory(); 203 gFavicon = gSvgFavicon; 204 205 let iconFile = getTempFile(); 206 gOverrideWindowsIconFileOnce = iconFile; 207 208 await TaskbarTabsPin.pinTaskbarTab(taskbarTab, registry); 209 210 ok( 211 kDefaultIconSpy.get.notCalled, 212 "The default icon should not be used when a vector favicon exists for the page." 213 ); 214 215 await testWrittenIconFile(iconFile); 216 217 shellPinCalled(taskbarTab); 218 }); 219 220 add_task(async function test_pin_missing_favicon() { 221 sinon.resetHistory(); 222 gFavicon = null; 223 await TaskbarTabsPin.pinTaskbarTab(taskbarTab, registry); 224 225 ok( 226 kMockFaviconService.getFaviconForPage.calledOnce, 227 "The favicon for the page should have attempted to be retrieved." 228 ); 229 ok( 230 kDefaultIconSpy.get.called, 231 "The default icon should be used when a favicon does not exist for the page." 232 ); 233 234 shellPinCalled(taskbarTab); 235 }); 236 237 add_task(async function test_pin_location() { 238 sinon.resetHistory(); 239 240 await TaskbarTabsPin.pinTaskbarTab(taskbarTab, registry); 241 const spy = kMockNativeShellService.createShortcut; 242 ok(spy.calledOnce, "A shortcut was created"); 243 Assert.equal( 244 spy.firstCall.args[6], 245 "Programs", 246 "The shortcut went into the Start Menu folder" 247 ); 248 Assert.equal( 249 spy.firstCall.args[7], 250 `[formatValue_taskbar-tab-shortcut-folder]\\${taskbarTab.name}.lnk`, 251 "The shortcut should be in a subdirectory and have a default name" 252 ); 253 254 Assert.equal( 255 taskbarTab.shortcutRelativePath, 256 spy.firstCall.args[7], 257 "Correct relative path was saved to the taskbar tab" 258 ); 259 Assert.equal(patchedSpy.callCount, 1, "A single patched event was emitted"); 260 }); 261 262 add_task(async function test_pin_location_dos_name() { 263 const parsedURI = Services.io.newURI("https://aux.test"); 264 const invalidTaskbarTab = createTaskbarTab(registry, parsedURI, 0); 265 sinon.resetHistory(); 266 267 await TaskbarTabsPin.pinTaskbarTab(invalidTaskbarTab, registry); 268 const spy = kMockNativeShellService.createShortcut; 269 ok(spy.calledOnce, "A shortcut was created"); 270 Assert.equal( 271 spy.firstCall.args[6], 272 "Programs", 273 "The shortcut went into the Start Menu folder" 274 ); 275 // 'Untitled' is the default selected by the MIME code, since 276 // AUX is a reserved name on Windows. 277 Assert.equal( 278 spy.firstCall.args[7], 279 "[formatValue_taskbar-tab-shortcut-folder]\\Untitled.lnk", 280 "The shortcut should be in a subdirectory and have a default name" 281 ); 282 Assert.equal( 283 invalidTaskbarTab.shortcutRelativePath, 284 spy.firstCall.args[7], 285 "Correct relative path was saved to the taskbar tab" 286 ); 287 Assert.equal(patchedSpy.callCount, 1, "A single patched event was emitted"); 288 289 registry.removeTaskbarTab(invalidTaskbarTab.id); 290 }); 291 292 add_task(async function test_pin_location_bad_characters() { 293 const parsedURI = Services.io.newURI("https://another.test"); 294 const invalidTaskbarTab = createTaskbarTab(registry, parsedURI, 0, { 295 manifest: { 296 name: "** :\t\r\n \\\\ >> Not a valid. filename??! << // |||: **.", 297 }, 298 }); 299 sinon.resetHistory(); 300 301 await TaskbarTabsPin.pinTaskbarTab(invalidTaskbarTab); 302 const spy = kMockNativeShellService.createShortcut; 303 ok(spy.calledOnce, "A shortcut was created"); 304 Assert.equal( 305 spy.firstCall.args[6], 306 "Programs", 307 "The shortcut went into the Start Menu folder" 308 ); 309 Assert.equal( 310 spy.firstCall.args[7], 311 "[formatValue_taskbar-tab-shortcut-folder]\\__ ____ __ __ Not a valid. filename__! __ __ ____ __..lnk", 312 "The shortcut should have invalid characters filtered out." 313 ); 314 registry.removeTaskbarTab(invalidTaskbarTab.id); 315 }); 316 317 add_task(async function test_pin_location_lnk_extension() { 318 const parsedURI = Services.io.newURI("https://another.test"); 319 const invalidTaskbarTab = createTaskbarTab(registry, parsedURI, 0, { 320 manifest: { 321 name: "coolstartup.lnk", 322 }, 323 }); 324 sinon.resetHistory(); 325 326 await TaskbarTabsPin.pinTaskbarTab(invalidTaskbarTab); 327 const spy = kMockNativeShellService.createShortcut; 328 ok(spy.calledOnce, "A shortcut was created"); 329 Assert.equal( 330 spy.firstCall.args[6], 331 "Programs", 332 "The shortcut went into the Start Menu folder" 333 ); 334 Assert.equal( 335 spy.firstCall.args[7], 336 "[formatValue_taskbar-tab-shortcut-folder]\\coolstartup.lnk.lnk", 337 "The shortcut should keep the .lnk intact." 338 ); 339 340 registry.removeTaskbarTab(invalidTaskbarTab.id); 341 }); 342 343 add_task(async function test_unpin() { 344 sinon.resetHistory(); 345 await TaskbarTabsPin.unpinTaskbarTab(taskbarTab, registry); 346 347 shellUnpinCalled(); 348 Assert.equal( 349 taskbarTab.shortcutRelativePath, 350 null, 351 "Shortcut relative path was removed from the taskbar tab" 352 ); 353 Assert.equal(patchedSpy.callCount, 1, "A single patched event was emitted"); 354 });