зеркало из https://github.com/mozilla/gecko-dev.git
Backed out 5 changesets (bug 1493104) for ESlint failure at builds/worker/checkouts/gecko/devtools/client/webide/content/addons.js on a CLOSED TREE
Backed out changeset a6cbba097c8a (bug 1493104) Backed out changeset 8df2a288391c (bug 1493104) Backed out changeset cb9f55dd4257 (bug 1493104) Backed out changeset f7ecf841eb8f (bug 1493104) Backed out changeset fa20c2c68c42 (bug 1493104)
This commit is contained in:
Родитель
2b20cddf86
Коммит
9a32880ddc
|
@ -4,36 +4,40 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
loader.lazyGetter(this, "adbScanner", () => {
|
||||
const { AddonAwareADBScanner } = require("devtools/shared/adb/addon-aware-adb-scanner");
|
||||
return new AddonAwareADBScanner();
|
||||
});
|
||||
const { ADBScanner } = require("devtools/shared/adb/adb-scanner");
|
||||
loader.lazyRequireGetter(this, "adbAddon", "devtools/shared/adb/adb-addon", true);
|
||||
loader.lazyRequireGetter(this, "ADB_ADDON_STATES", "devtools/shared/adb/adb-addon", true);
|
||||
|
||||
/**
|
||||
* This module provides a collection of helper methods to detect USB runtimes whom Firefox
|
||||
* is running on.
|
||||
*/
|
||||
function addUSBRuntimesObserver(listener) {
|
||||
adbScanner.on("runtime-list-updated", listener);
|
||||
ADBScanner.on("runtime-list-updated", listener);
|
||||
}
|
||||
exports.addUSBRuntimesObserver = addUSBRuntimesObserver;
|
||||
|
||||
function disableUSBRuntimes() {
|
||||
adbScanner.disable();
|
||||
ADBScanner.disable();
|
||||
}
|
||||
exports.disableUSBRuntimes = disableUSBRuntimes;
|
||||
|
||||
async function enableUSBRuntimes() {
|
||||
adbScanner.enable();
|
||||
if (adbAddon.status !== ADB_ADDON_STATES.INSTALLED) {
|
||||
console.error("ADB extension is not installed");
|
||||
return;
|
||||
}
|
||||
|
||||
ADBScanner.enable();
|
||||
}
|
||||
exports.enableUSBRuntimes = enableUSBRuntimes;
|
||||
|
||||
function getUSBRuntimes() {
|
||||
return adbScanner.listRuntimes();
|
||||
return ADBScanner.listRuntimes();
|
||||
}
|
||||
exports.getUSBRuntimes = getUSBRuntimes;
|
||||
|
||||
function removeUSBRuntimesObserver(listener) {
|
||||
adbScanner.off("runtime-list-updated", listener);
|
||||
ADBScanner.off("runtime-list-updated", listener);
|
||||
}
|
||||
exports.removeUSBRuntimesObserver = removeUSBRuntimesObserver;
|
||||
|
|
|
@ -8,6 +8,8 @@ const Services = require("Services");
|
|||
const Strings = Services.strings.createBundle("chrome://devtools/locale/webide.properties");
|
||||
|
||||
const {gDevTools} = require("devtools/client/framework/devtools");
|
||||
const {ADBScanner} = require("devtools/shared/adb/adb-scanner");
|
||||
const {RuntimeScanners} = require("devtools/client/webide/modules/runtimes");
|
||||
|
||||
loader.lazyRequireGetter(this, "adbAddon", "devtools/shared/adb/adb-addon", true);
|
||||
loader.lazyRequireGetter(this, "ADB_ADDON_STATES", "devtools/shared/adb/adb-addon", true);
|
||||
|
@ -32,6 +34,11 @@ function BuildUI() {
|
|||
progress.removeAttribute("value");
|
||||
li.setAttribute("status", adbAddon.status);
|
||||
status.textContent = Strings.GetStringFromName("addons_status_" + adbAddon.status);
|
||||
if (adbAddon.status == ADB_ADDON_STATES.INSTALLED) {
|
||||
RuntimeScanners.add(ADBScanner);
|
||||
} else if (adbAddon.status == ADB_ADDON_STATES.UNINSTALLED) {
|
||||
RuntimeScanners.remove(ADBScanner);
|
||||
}
|
||||
}
|
||||
|
||||
function onAddonFailure(arg) {
|
||||
|
|
|
@ -10,12 +10,6 @@ const discovery = require("devtools/shared/discovery/discovery");
|
|||
const EventEmitter = require("devtools/shared/event-emitter");
|
||||
const {RuntimeTypes} = require("devtools/client/webide/modules/runtime-types");
|
||||
const promise = require("promise");
|
||||
|
||||
loader.lazyGetter(this, "adbScanner", () => {
|
||||
const { AddonAwareADBScanner } = require("devtools/shared/adb/addon-aware-adb-scanner");
|
||||
return new AddonAwareADBScanner();
|
||||
});
|
||||
|
||||
loader.lazyRequireGetter(this, "AuthenticationResult",
|
||||
"devtools/shared/security/auth", true);
|
||||
loader.lazyRequireGetter(this, "DevToolsUtils",
|
||||
|
@ -198,10 +192,6 @@ exports.RuntimeScanners = RuntimeScanners;
|
|||
|
||||
/* SCANNERS */
|
||||
|
||||
// The adb-scanner will automatically start and stop when the ADB extension is installed
|
||||
// and uninstalled, so the scanner itself can always be used.
|
||||
RuntimeScanners.add(adbScanner);
|
||||
|
||||
var WiFiScanner = {
|
||||
|
||||
_runtimes: [],
|
||||
|
|
|
@ -12,20 +12,17 @@ const { RuntimeTypes } =
|
|||
const { ADB } = require("devtools/shared/adb/adb");
|
||||
loader.lazyRequireGetter(this, "Device", "devtools/shared/adb/adb-device");
|
||||
|
||||
class ADBScanner extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
this._runtimes = [];
|
||||
const ADBScanner = {
|
||||
|
||||
this._onDeviceConnected = this._onDeviceConnected.bind(this);
|
||||
this._onDeviceDisconnected = this._onDeviceDisconnected.bind(this);
|
||||
this._updateRuntimes = this._updateRuntimes.bind(this);
|
||||
}
|
||||
_runtimes: [],
|
||||
|
||||
enable() {
|
||||
this._onDeviceConnected = this._onDeviceConnected.bind(this);
|
||||
this._onDeviceDisconnected = this._onDeviceDisconnected.bind(this);
|
||||
EventEmitter.on(ADB, "device-connected", this._onDeviceConnected);
|
||||
EventEmitter.on(ADB, "device-disconnected", this._onDeviceDisconnected);
|
||||
|
||||
this._updateRuntimes = this._updateRuntimes.bind(this);
|
||||
Devices.on("register", this._updateRuntimes);
|
||||
Devices.on("unregister", this._updateRuntimes);
|
||||
Devices.on("addon-status-updated", this._updateRuntimes);
|
||||
|
@ -34,7 +31,7 @@ class ADBScanner extends EventEmitter {
|
|||
ADB.trackDevices();
|
||||
});
|
||||
this._updateRuntimes();
|
||||
}
|
||||
},
|
||||
|
||||
disable() {
|
||||
EventEmitter.off(ADB, "device-connected", this._onDeviceConnected);
|
||||
|
@ -42,20 +39,20 @@ class ADBScanner extends EventEmitter {
|
|||
Devices.off("register", this._updateRuntimes);
|
||||
Devices.off("unregister", this._updateRuntimes);
|
||||
Devices.off("addon-status-updated", this._updateRuntimes);
|
||||
}
|
||||
},
|
||||
|
||||
_emitUpdated() {
|
||||
this.emit("runtime-list-updated");
|
||||
}
|
||||
},
|
||||
|
||||
_onDeviceConnected(deviceId) {
|
||||
const device = new Device(deviceId);
|
||||
Devices.register(deviceId, device);
|
||||
}
|
||||
},
|
||||
|
||||
_onDeviceDisconnected(deviceId) {
|
||||
Devices.unregister(deviceId);
|
||||
}
|
||||
},
|
||||
|
||||
_updateRuntimes() {
|
||||
if (this._updatingPromise) {
|
||||
|
@ -75,23 +72,26 @@ class ADBScanner extends EventEmitter {
|
|||
this._updatingPromise = null;
|
||||
});
|
||||
return this._updatingPromise;
|
||||
}
|
||||
},
|
||||
|
||||
async _detectRuntimes(device) {
|
||||
_detectRuntimes: async function(device) {
|
||||
const model = await device.getModel();
|
||||
const detectedRuntimes =
|
||||
await FirefoxOnAndroidRuntime.detect(device, model);
|
||||
this._runtimes.push(...detectedRuntimes);
|
||||
}
|
||||
},
|
||||
|
||||
scan() {
|
||||
return this._updateRuntimes();
|
||||
}
|
||||
},
|
||||
|
||||
listRuntimes() {
|
||||
return this._runtimes;
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
EventEmitter.decorate(ADBScanner);
|
||||
|
||||
function Runtime(device, model, socketPath) {
|
||||
this.device = device;
|
||||
|
|
|
@ -1,98 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const EventEmitter = require("devtools/shared/event-emitter");
|
||||
|
||||
loader.lazyRequireGetter(this, "adbAddon", "devtools/shared/adb/adb-addon", true);
|
||||
loader.lazyRequireGetter(this, "ADBScanner", "devtools/shared/adb/adb-scanner", true);
|
||||
|
||||
/**
|
||||
* The AddonAwareADBScanner decorates an instance of ADBScanner. It will wait until the
|
||||
* ADB addon is installed to enable() the real scanner, and will automatically disable
|
||||
* it if the addon is uninstalled.
|
||||
*
|
||||
* It implements the following public API of ADBScanner:
|
||||
* - enable
|
||||
* - disable
|
||||
* - scan
|
||||
* - listRuntimes
|
||||
* - event "runtime-list-updated"
|
||||
*/
|
||||
class AddonAwareADBScanner extends EventEmitter {
|
||||
/**
|
||||
* Parameters are provided only to allow tests to replace actual implementations with
|
||||
* mocks.
|
||||
*
|
||||
* @param {ADBScanner} scanner
|
||||
* Only provided in tests for mocks
|
||||
* @param {ADBAddon} addon
|
||||
* Only provided in tests for mocks
|
||||
*/
|
||||
constructor(scanner = new ADBScanner(), addon = adbAddon) {
|
||||
super();
|
||||
|
||||
this._onScannerListUpdated = this._onScannerListUpdated.bind(this);
|
||||
this._onAddonUpdate = this._onAddonUpdate.bind(this);
|
||||
|
||||
this._scanner = scanner;
|
||||
this._scanner.on("runtime-list-updated", this._onScannerListUpdated);
|
||||
|
||||
this._addon = addon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only forward the enable() call if the addon is installed, because ADBScanner::enable
|
||||
* only works if the addon is installed.
|
||||
*/
|
||||
enable() {
|
||||
if (this._addon.status === "installed") {
|
||||
this._scanner.enable();
|
||||
}
|
||||
|
||||
// Remove any previous listener, to make sure we only add one listener if enable() is
|
||||
// called several times.
|
||||
this._addon.off("update", this._onAddonUpdate);
|
||||
|
||||
this._addon.on("update", this._onAddonUpdate);
|
||||
}
|
||||
|
||||
disable() {
|
||||
this._scanner.disable();
|
||||
|
||||
this._addon.off("update", this._onAddonUpdate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan for USB devices.
|
||||
*
|
||||
* @return {Promise} Promise that will resolve when the scan is completed.
|
||||
*/
|
||||
scan() {
|
||||
return this._scanner.scan();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of currently detected runtimes.
|
||||
*
|
||||
* @return {Array} Array of currently detected runtimes.
|
||||
*/
|
||||
listRuntimes() {
|
||||
return this._scanner.listRuntimes();
|
||||
}
|
||||
|
||||
_onAddonUpdate() {
|
||||
if (this._addon.status === "installed") {
|
||||
this._scanner.enable();
|
||||
} else {
|
||||
this._scanner.disable();
|
||||
}
|
||||
}
|
||||
|
||||
_onScannerListUpdated() {
|
||||
this.emit("runtime-list-updated");
|
||||
}
|
||||
}
|
||||
exports.AddonAwareADBScanner = AddonAwareADBScanner;
|
|
@ -11,7 +11,6 @@ DevToolsModules(
|
|||
'adb-scanner.js',
|
||||
'adb-socket.js',
|
||||
'adb.js',
|
||||
'addon-aware-adb-scanner.js',
|
||||
)
|
||||
|
||||
with Files('**'):
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
const EventEmitter = require("devtools/shared/event-emitter");
|
||||
|
|
|
@ -1,221 +0,0 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
const EventEmitter = require("devtools/shared/event-emitter");
|
||||
const { AddonAwareADBScanner } = require("devtools/shared/adb/addon-aware-adb-scanner");
|
||||
|
||||
/**
|
||||
* For the scanner mock, we create an object with spies for each of the public methods
|
||||
* used by the AddonAwareADBScanner, and the ability to emit events.
|
||||
*/
|
||||
function prepareMockScanner() {
|
||||
const mockScanner = {
|
||||
enable: sinon.spy(),
|
||||
disable: sinon.spy(),
|
||||
scan: sinon.spy(),
|
||||
listRuntimes: sinon.spy()
|
||||
};
|
||||
EventEmitter.decorate(mockScanner);
|
||||
return mockScanner;
|
||||
}
|
||||
|
||||
/**
|
||||
* For the addon mock, we simply need an object that is able to emit events and has a
|
||||
* status.
|
||||
*/
|
||||
function prepareMockAddon() {
|
||||
const mockAddon = {
|
||||
status: "unknown",
|
||||
};
|
||||
EventEmitter.decorate(mockAddon);
|
||||
return mockAddon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare all mocks needed for the scanner tests.
|
||||
*/
|
||||
function prepareMocks() {
|
||||
const mockScanner = prepareMockScanner();
|
||||
const mockAddon = prepareMockAddon();
|
||||
const addonAwareAdbScanner = new AddonAwareADBScanner(mockScanner, mockAddon);
|
||||
return { addonAwareAdbScanner, mockAddon, mockScanner };
|
||||
}
|
||||
|
||||
/**
|
||||
* This test covers basic usage of enable() on the AddonAwareADBScanner, and checks the
|
||||
* different behaviors based on the addon status.
|
||||
*/
|
||||
add_task(async function testCallingEnable() {
|
||||
const { mockScanner, mockAddon, addonAwareAdbScanner } = prepareMocks();
|
||||
|
||||
// Check that enable() is not called if the addon is uninstalled
|
||||
mockAddon.status = "uninstalled";
|
||||
addonAwareAdbScanner.enable();
|
||||
ok(mockScanner.enable.notCalled, "enable() was not called");
|
||||
mockScanner.enable.reset();
|
||||
|
||||
// Check that enable() is called if the addon is installed
|
||||
mockAddon.status = "installed";
|
||||
addonAwareAdbScanner.enable();
|
||||
ok(mockScanner.enable.called, "enable() was called");
|
||||
mockScanner.enable.reset();
|
||||
});
|
||||
|
||||
/**
|
||||
* This test checks that enable()/disable() methods from the internal ADBScanner are
|
||||
* called when the addon is installed or uninstalled.
|
||||
*/
|
||||
add_task(async function testUpdatingAddonEnablesDisablesScanner() {
|
||||
const { mockScanner, mockAddon, addonAwareAdbScanner } = prepareMocks();
|
||||
|
||||
// Enable the addon aware scanner
|
||||
addonAwareAdbScanner.enable();
|
||||
ok(mockScanner.enable.notCalled, "enable() was not called initially");
|
||||
|
||||
// Check that enable() is called automatically when the addon is installed
|
||||
mockAddon.status = "installed";
|
||||
mockAddon.emit("update");
|
||||
ok(mockScanner.enable.called, "enable() was called when installing the addon");
|
||||
ok(mockScanner.disable.notCalled, "disable() was not called when installing the addon");
|
||||
mockScanner.enable.reset();
|
||||
mockScanner.disable.reset();
|
||||
|
||||
// Check that disabled() is called automatically when the addon is uninstalled
|
||||
mockAddon.status = "uninstalled";
|
||||
mockAddon.emit("update");
|
||||
ok(mockScanner.enable.notCalled, "enable() was not called when uninstalling the addon");
|
||||
ok(mockScanner.disable.called, "disable() was called when uninstalling the addon");
|
||||
mockScanner.enable.reset();
|
||||
mockScanner.disable.reset();
|
||||
|
||||
// Check that enable() is called again when the addon is reinstalled
|
||||
mockAddon.status = "installed";
|
||||
mockAddon.emit("update");
|
||||
ok(mockScanner.enable.called, "enable() was called when installing the addon");
|
||||
ok(mockScanner.disable.notCalled, "disable() was not called when installing the addon");
|
||||
mockScanner.enable.reset();
|
||||
mockScanner.disable.reset();
|
||||
});
|
||||
|
||||
/**
|
||||
* This test checks that disable() is forwarded from the AddonAwareADBScanner to the real
|
||||
* scanner even if the addon is uninstalled. We might miss the addon uninstall
|
||||
* notification, so it is safer to always proceed with disabling.
|
||||
*/
|
||||
add_task(async function testScannerIsDisabledWhenMissingAddonUpdate() {
|
||||
const { mockScanner, mockAddon, addonAwareAdbScanner } = prepareMocks();
|
||||
|
||||
// Enable the addon aware scanner
|
||||
mockAddon.status = "installed";
|
||||
addonAwareAdbScanner.enable();
|
||||
ok(mockScanner.enable.called, "enable() was called initially");
|
||||
mockScanner.enable.reset();
|
||||
|
||||
// Uninstall the addon without firing any event
|
||||
mockAddon.status = "uninstalled";
|
||||
|
||||
// Programmatically call disable, check that the scanner's disable is called even though
|
||||
// the addon was uninstalled.
|
||||
addonAwareAdbScanner.disable();
|
||||
ok(mockScanner.disable.called, "disable() was called when uninstalling the addon");
|
||||
mockScanner.disable.reset();
|
||||
});
|
||||
|
||||
/**
|
||||
* This test checks that when the AddonAwareADBScanner is disabled, then enable/disable
|
||||
* are not called on the inner scanner when the addon status changes.
|
||||
*/
|
||||
add_task(async function testInnerEnableIsNotCalledIfNotStarted() {
|
||||
const { mockScanner, mockAddon, addonAwareAdbScanner } = prepareMocks();
|
||||
|
||||
// Check that enable() is not called on the inner scanner when the addon is installed
|
||||
// if the AddonAwareADBScanner was not enabled
|
||||
mockAddon.status = "installed";
|
||||
mockAddon.emit("update");
|
||||
ok(mockScanner.enable.notCalled, "enable() was not called");
|
||||
|
||||
// Same for disable() and "uninstall"
|
||||
mockAddon.status = "uninstalled";
|
||||
mockAddon.emit("update");
|
||||
ok(mockScanner.disable.notCalled, "disable() was not called");
|
||||
|
||||
// Enable the addon aware scanner
|
||||
addonAwareAdbScanner.enable();
|
||||
ok(mockScanner.enable.notCalled, "enable() was not called");
|
||||
ok(mockScanner.disable.notCalled, "disable() was not called");
|
||||
});
|
||||
|
||||
/**
|
||||
* This test checks that when the AddonAwareADBScanner is disabled, installing the addon
|
||||
* no longer enables the internal ADBScanner.
|
||||
*/
|
||||
add_task(async function testEnableIsNoLongerCalledAfterDisabling() {
|
||||
const { mockScanner, mockAddon, addonAwareAdbScanner } = prepareMocks();
|
||||
|
||||
// Start with the addon installed
|
||||
mockAddon.status = "installed";
|
||||
addonAwareAdbScanner.enable();
|
||||
ok(mockScanner.enable.called, "enable() was called since addon was already installed");
|
||||
mockScanner.enable.reset();
|
||||
|
||||
// Here we call enable again to check that we will not add too many events.
|
||||
// A single call to disable() should stop all listeners, even if we called enable()
|
||||
// several times.
|
||||
addonAwareAdbScanner.enable();
|
||||
ok(mockScanner.enable.called, "enable() was called again");
|
||||
mockScanner.enable.reset();
|
||||
|
||||
// Disable the scanner
|
||||
addonAwareAdbScanner.disable();
|
||||
ok(mockScanner.disable.called, "disable() was called");
|
||||
mockScanner.disable.reset();
|
||||
|
||||
// Emit an addon update event
|
||||
mockAddon.emit("update");
|
||||
ok(mockScanner.enable.notCalled,
|
||||
"enable() is not called since the main scanner is disabled");
|
||||
});
|
||||
|
||||
/**
|
||||
* Basic check that the "runtime-list-updated" event is forwarded.
|
||||
*/
|
||||
add_task(async function testListUpdatedEventForwarding() {
|
||||
const { mockScanner, addonAwareAdbScanner } = prepareMocks();
|
||||
|
||||
const spy = sinon.spy();
|
||||
addonAwareAdbScanner.on("runtime-list-updated", spy);
|
||||
mockScanner.emit("runtime-list-updated");
|
||||
ok(spy.called, "The runtime-list-updated event was forwarded from ADBScanner");
|
||||
addonAwareAdbScanner.off("runtime-list-updated", spy);
|
||||
});
|
||||
|
||||
/**
|
||||
* Basic check that calls to scan() are forwarded.
|
||||
*/
|
||||
add_task(async function testScanCallForwarding() {
|
||||
const { mockScanner, addonAwareAdbScanner } = prepareMocks();
|
||||
|
||||
ok(mockScanner.scan.notCalled, "ADBScanner scan() is not called initially");
|
||||
|
||||
addonAwareAdbScanner.scan();
|
||||
mockScanner.emit("runtime-list-updated");
|
||||
ok(mockScanner.scan.called, "ADBScanner scan() was called");
|
||||
mockScanner.scan.reset();
|
||||
});
|
||||
|
||||
/**
|
||||
* Basic check that calls to scan() are forwarded.
|
||||
*/
|
||||
add_task(async function testListRuntimesCallForwarding() {
|
||||
const { mockScanner, addonAwareAdbScanner } = prepareMocks();
|
||||
|
||||
ok(mockScanner.listRuntimes.notCalled,
|
||||
"ADBScanner listRuntimes() is not called initially");
|
||||
|
||||
addonAwareAdbScanner.listRuntimes();
|
||||
mockScanner.emit("runtime-list-updated");
|
||||
ok(mockScanner.listRuntimes.called, "ADBScanner listRuntimes() was called");
|
||||
mockScanner.scan.reset();
|
||||
});
|
|
@ -6,12 +6,3 @@
|
|||
/* eslint no-unused-vars: [2, {"vars": "local"}] */
|
||||
|
||||
const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm", {});
|
||||
const Services = require("Services");
|
||||
|
||||
// ================================================
|
||||
// Load mocking/stubbing library, sinon
|
||||
// docs: http://sinonjs.org/releases/v2.3.2/
|
||||
ChromeUtils.import("resource://gre/modules/Timer.jsm");
|
||||
Services.scriptloader.loadSubScript("resource://testing-common/sinon-2.3.2.js", this);
|
||||
/* globals sinon */
|
||||
// ================================================
|
||||
|
|
|
@ -8,4 +8,3 @@ support-files =
|
|||
|
||||
[test_adb.js]
|
||||
run-sequentially = An extension having the same id is installed/uninstalled in different tests
|
||||
[test_addon-aware-adb-scanner.js]
|
||||
|
|
Загрузка…
Ссылка в новой задаче