зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1314057 - Remove new-debugger-frontend usage from webconsole tests r=davidwalsh
--HG-- extra : histedit_source : 19a4c6ae70a0fd92828a6d2510921e11890c4cd2
This commit is contained in:
Родитель
1226150238
Коммит
817d38c9bc
|
@ -6,7 +6,6 @@
|
|||
|
||||
var Services = require("Services");
|
||||
var { gDevTools } = require("devtools/client/framework/devtools");
|
||||
var { getSourceText } = require("devtools/client/debugger/content/queries");
|
||||
|
||||
/**
|
||||
* Tries to open a Stylesheet file in the Style Editor. If the file is not
|
||||
|
@ -51,73 +50,14 @@ exports.viewSourceInStyleEditor = async function(toolbox, sourceURL,
|
|||
*/
|
||||
exports.viewSourceInDebugger = async function(toolbox, sourceURL, sourceLine,
|
||||
reason = "unknown") {
|
||||
// If the Debugger was already open, switch to it and try to show the
|
||||
// source immediately. Otherwise, initialize it and wait for the sources
|
||||
// to be added first.
|
||||
const debuggerAlreadyOpen = toolbox.getPanel("jsdebugger");
|
||||
const dbg = await toolbox.loadTool("jsdebugger");
|
||||
|
||||
// New debugger frontend
|
||||
if (Services.prefs.getBoolPref("devtools.debugger.new-debugger-frontend")) {
|
||||
const source = dbg.getSource(sourceURL);
|
||||
if (source) {
|
||||
await toolbox.selectTool("jsdebugger", reason);
|
||||
dbg.selectSource(sourceURL, sourceLine);
|
||||
return true;
|
||||
}
|
||||
|
||||
exports.viewSource(toolbox, sourceURL, sourceLine);
|
||||
return false;
|
||||
}
|
||||
|
||||
const win = dbg.panelWin;
|
||||
|
||||
// Old debugger frontend
|
||||
if (!debuggerAlreadyOpen) {
|
||||
await win.DebuggerController.waitForSourcesLoaded();
|
||||
}
|
||||
|
||||
const { DebuggerView } = win;
|
||||
const { Sources } = DebuggerView;
|
||||
|
||||
const item = Sources.getItemForAttachment(a => a.source.url === sourceURL);
|
||||
if (item) {
|
||||
const source = dbg.getSource(sourceURL);
|
||||
if (source) {
|
||||
await toolbox.selectTool("jsdebugger", reason);
|
||||
|
||||
// Determine if the source has already finished loading. There's two cases
|
||||
// in which we need to wait for the source to be shown:
|
||||
// 1) The requested source is not yet selected and will be shown once it is
|
||||
// selected and loaded
|
||||
// 2) The requested source is selected BUT the source text is still loading.
|
||||
const { actor } = item.attachment.source;
|
||||
const state = win.DebuggerController.getState();
|
||||
|
||||
// (1) Is the source selected?
|
||||
const selected = state.sources.selectedSource;
|
||||
const isSelected = selected === actor;
|
||||
|
||||
// (2) Has the source text finished loading?
|
||||
let isLoading = false;
|
||||
|
||||
// Only check if the source is loading when the source is already selected.
|
||||
// If the source is not selected, we will select it below and the already
|
||||
// pending load will be cancelled and this check is useless.
|
||||
if (isSelected) {
|
||||
const sourceTextInfo = getSourceText(state, selected);
|
||||
isLoading = sourceTextInfo && sourceTextInfo.loading;
|
||||
}
|
||||
|
||||
// Select the requested source
|
||||
DebuggerView.setEditorLocation(actor, sourceLine, { noDebug: true });
|
||||
|
||||
// Wait for it to load
|
||||
if (!isSelected || isLoading) {
|
||||
await win.DebuggerController.waitForSourceShown(sourceURL);
|
||||
}
|
||||
dbg.selectSource(sourceURL, sourceLine);
|
||||
return true;
|
||||
}
|
||||
|
||||
// If not found, still attempt to open in View Source
|
||||
exports.viewSource(toolbox, sourceURL, sourceLine);
|
||||
return false;
|
||||
};
|
||||
|
|
|
@ -253,7 +253,6 @@ tags = mcb
|
|||
[browser_webconsole_close_unfocused_window.js]
|
||||
[browser_webconsole_closing_after_completion.js]
|
||||
[browser_webconsole_close_sidebar.js]
|
||||
[browser_webconsole_closure_inspection.js]
|
||||
skip-if = true # Bug 1405250
|
||||
[browser_webconsole_console_api_iframe.js]
|
||||
[browser_webconsole_console_dir.js]
|
||||
|
|
|
@ -8,6 +8,12 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
// Import helpers for the new debugger
|
||||
/* import-globals-from ../../../debugger/new/test/mochitest/helpers.js */
|
||||
Services.scriptloader.loadSubScript(
|
||||
"chrome://mochitests/content/browser/devtools/client/debugger/new/test/mochitest/helpers.js",
|
||||
this);
|
||||
|
||||
const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
|
||||
"test/mochitest/test-autocomplete-in-stackframe.html";
|
||||
|
||||
|
@ -21,9 +27,6 @@ add_task(async function() {
|
|||
});
|
||||
|
||||
async function performTests() {
|
||||
// Force the old debugger UI since it's directly used (see Bug 1301705)
|
||||
await pushPref("devtools.debugger.new-debugger-frontend", false);
|
||||
|
||||
const { jsterm } = await openNewTabAndConsole(TEST_URI);
|
||||
const {
|
||||
autocompletePopup: popup,
|
||||
|
@ -60,10 +63,12 @@ async function performTests() {
|
|||
`"foo1Obj.prop2." gave the expected suggestions`);
|
||||
|
||||
info("Opening Debugger");
|
||||
const {panel} = await openDebugger();
|
||||
await openDebugger();
|
||||
const dbg = createDebuggerContext(toolbox);
|
||||
|
||||
info("Waiting for pause");
|
||||
const stackFrames = await pauseDebugger(panel);
|
||||
await pauseDebugger(dbg);
|
||||
const stackFrames = dbg.selectors.getCallStackFrames(dbg.getState());
|
||||
|
||||
info("Opening Console again");
|
||||
await toolbox.selectTool("webconsole");
|
||||
|
@ -77,7 +82,7 @@ async function performTests() {
|
|||
await openDebugger();
|
||||
|
||||
// Select the frame for the `firstCall` function.
|
||||
stackFrames.selectFrame(1);
|
||||
await dbg.actions.selectFrame(stackFrames[1]);
|
||||
|
||||
info("openConsole");
|
||||
await toolbox.selectTool("webconsole");
|
||||
|
@ -109,18 +114,10 @@ function getPopupLabels(popup) {
|
|||
return popup.getItems().map(item => item.label);
|
||||
}
|
||||
|
||||
function pauseDebugger(debuggerPanel) {
|
||||
const debuggerWin = debuggerPanel.panelWin;
|
||||
const debuggerController = debuggerWin.DebuggerController;
|
||||
const thread = debuggerController.activeThread;
|
||||
|
||||
return new Promise(resolve => {
|
||||
thread.addOneTimeListener("framesadded", () =>
|
||||
resolve(debuggerController.StackFrames));
|
||||
|
||||
info("firstCall()");
|
||||
ContentTask.spawn(gBrowser.selectedBrowser, {}, function() {
|
||||
content.wrappedJSObject.firstCall();
|
||||
});
|
||||
async function pauseDebugger(dbg) {
|
||||
info("Waiting for debugger to pause");
|
||||
ContentTask.spawn(gBrowser.selectedBrowser, {}, async function() {
|
||||
content.wrappedJSObject.firstCall();
|
||||
});
|
||||
await waitForPaused(dbg);
|
||||
}
|
||||
|
|
|
@ -7,19 +7,27 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
// Import helpers for the new debugger
|
||||
/* import-globals-from ../../../debugger/new/test/mochitest/helpers.js */
|
||||
Services.scriptloader.loadSubScript(
|
||||
"chrome://mochitests/content/browser/devtools/client/debugger/new/test/mochitest/helpers.js",
|
||||
this);
|
||||
|
||||
requestLongerTimeout(5);
|
||||
|
||||
const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
|
||||
"test/mochitest/" +
|
||||
"test-click-function-to-source.html";
|
||||
|
||||
// Force the old debugger UI since it's directly used (see Bug 1301705)
|
||||
pushPref("devtools.debugger.new-debugger-frontend", false);
|
||||
const TEST_SCRIPT_URI = "http://example.com/browser/devtools/client/webconsole/" +
|
||||
"test/mochitest/" +
|
||||
"test-click-function-to-source.js";
|
||||
|
||||
add_task(async function() {
|
||||
const hud = await openNewTabAndConsole(TEST_URI);
|
||||
|
||||
info("Open the Debugger panel.");
|
||||
const {panel} = await openDebugger();
|
||||
const panelWin = panel.panelWin;
|
||||
await openDebugger();
|
||||
|
||||
info("And right after come back to the Console panel.");
|
||||
await openConsole();
|
||||
|
@ -34,13 +42,13 @@ add_task(async function() {
|
|||
ok(jumpIcon, "A jump to definition button is rendered, as expected");
|
||||
|
||||
info("Click on the jump to definition button.");
|
||||
const onEditorLocationSet = panelWin.once(panelWin.EVENTS.EDITOR_LOCATION_SET);
|
||||
jumpIcon.click();
|
||||
await onEditorLocationSet;
|
||||
|
||||
const {editor} = panelWin.DebuggerView;
|
||||
const {line, ch} = editor.getCursor();
|
||||
// Source editor starts counting line and column numbers from 0.
|
||||
is(line, 8, "Debugger is open at the expected line");
|
||||
is(ch, 0, "Debugger is open at the expected character");
|
||||
const toolbox = gDevTools.getToolbox(hud.target);
|
||||
const dbg = createDebuggerContext(toolbox);
|
||||
await waitForSelectedSource(dbg, TEST_SCRIPT_URI);
|
||||
|
||||
const pendingLocation = dbg.selectors.getPendingSelectedLocation(dbg.getState());
|
||||
const {line} = pendingLocation;
|
||||
is(line, 9, "Debugger is open at the expected line");
|
||||
});
|
||||
|
|
|
@ -1,104 +0,0 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// XXX Remove this when the file is migrated to the new frontend.
|
||||
/* eslint-disable no-undef */
|
||||
|
||||
// Check that inspecting a closure in the variables view sidebar works when
|
||||
// execution is paused.
|
||||
|
||||
"use strict";
|
||||
|
||||
const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
|
||||
"test/mochitest/test-closures.html";
|
||||
|
||||
var gWebConsole, gJSTerm, gVariablesView;
|
||||
|
||||
// Force the old debugger UI since it's directly used (see Bug 1301705)
|
||||
Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
|
||||
registerCleanupFunction(function() {
|
||||
Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
|
||||
});
|
||||
|
||||
function test() {
|
||||
registerCleanupFunction(() => {
|
||||
gWebConsole = gJSTerm = gVariablesView = null;
|
||||
});
|
||||
|
||||
function fetchScopes(hud, toolbox, panelWin, deferred) {
|
||||
panelWin.once(panelWin.EVENTS.FETCHED_SCOPES, () => {
|
||||
ok(true, "Scopes were fetched");
|
||||
toolbox.selectTool("webconsole").then(() => consoleOpened(hud));
|
||||
deferred.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
loadTab(TEST_URI).then(() => {
|
||||
openConsole().then((hud) => {
|
||||
openDebugger().then(({ toolbox, panelWin }) => {
|
||||
const deferred = defer();
|
||||
fetchScopes(hud, toolbox, panelWin, deferred);
|
||||
|
||||
// eslint-disable-next-line
|
||||
ContentTask.spawn(gBrowser.selectedBrowser, {}, () => {
|
||||
const button = content.document.querySelector("button");
|
||||
ok(button, "button element found");
|
||||
button.click();
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function consoleOpened(hud) {
|
||||
gWebConsole = hud;
|
||||
gJSTerm = hud.jsterm;
|
||||
gJSTerm.execute("window.george.getName");
|
||||
|
||||
waitForMessages({
|
||||
webconsole: gWebConsole,
|
||||
messages: [{
|
||||
text: "getName()",
|
||||
category: CATEGORY_OUTPUT,
|
||||
objects: true,
|
||||
}],
|
||||
}).then(onExecuteGetName);
|
||||
}
|
||||
|
||||
function onExecuteGetName(results) {
|
||||
const clickable = results[0].clickableElements[0];
|
||||
ok(clickable, "clickable object found");
|
||||
|
||||
gJSTerm.once("variablesview-fetched", onGetNameFetch);
|
||||
const contextMenu =
|
||||
gWebConsole.iframeWindow.document.getElementById("output-contextmenu");
|
||||
waitForContextMenu(contextMenu, clickable, () => {
|
||||
const openInVarView = contextMenu.querySelector("#menu_openInVarView");
|
||||
ok(openInVarView.disabled === false,
|
||||
"the \"Open In Variables View\" context menu item should be clickable");
|
||||
// EventUtils.synthesizeMouseAtCenter seems to fail here in Mac OSX
|
||||
openInVarView.click();
|
||||
});
|
||||
}
|
||||
|
||||
function onGetNameFetch(view) {
|
||||
gVariablesView = view._variablesView;
|
||||
ok(gVariablesView, "variables view object");
|
||||
|
||||
findVariableViewProperties(view, [
|
||||
{ name: /_pfactory/, value: "" },
|
||||
], { webconsole: gWebConsole }).then(onExpandClosure);
|
||||
}
|
||||
|
||||
function onExpandClosure(results) {
|
||||
const prop = results[0].matchedProp;
|
||||
ok(prop, "matched the name property in the variables view");
|
||||
|
||||
gVariablesView.window.focus();
|
||||
gJSTerm.once("sidebar-closed", finishTest);
|
||||
EventUtils.synthesizeKey("VK_ESCAPE", {}, gVariablesView.window);
|
||||
}
|
|
@ -8,13 +8,16 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
// Import helpers for the new debugger
|
||||
/* import-globals-from ../../../debugger/new/test/mochitest/helpers.js */
|
||||
Services.scriptloader.loadSubScript(
|
||||
"chrome://mochitests/content/browser/devtools/client/debugger/new/test/mochitest/helpers.js",
|
||||
this);
|
||||
|
||||
const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
|
||||
"test/mochitest/test-eval-in-stackframe.html";
|
||||
|
||||
add_task(async function() {
|
||||
// Force the old debugger UI since it's directly used (see Bug 1301705).
|
||||
await pushPref("devtools.debugger.new-debugger-frontend", false);
|
||||
|
||||
info("open the console");
|
||||
const hud = await openNewTabAndConsole(TEST_URI);
|
||||
const {jsterm} = hud;
|
||||
|
@ -32,8 +35,9 @@ add_task(async function() {
|
|||
ok(true, "'newFoo' is displayed after adding `foo2`");
|
||||
|
||||
info("Open the debugger and then select the console again");
|
||||
const {panel} = await openDebugger();
|
||||
const {activeThread, StackFrames: stackFrames} = panel.panelWin.DebuggerController;
|
||||
await openDebugger();
|
||||
const toolbox = gDevTools.getToolbox(hud.target);
|
||||
const dbg = createDebuggerContext(toolbox);
|
||||
|
||||
await openConsole();
|
||||
|
||||
|
@ -44,13 +48,9 @@ add_task(async function() {
|
|||
|
||||
info("Select the debugger again");
|
||||
await openDebugger();
|
||||
await pauseDebugger(dbg);
|
||||
|
||||
const onFirstCallFramesAdded = activeThread.addOneTimeListener("framesadded");
|
||||
// firstCall calls secondCall, which has a debugger statement, so we'll be paused.
|
||||
ContentTask.spawn(gBrowser.selectedBrowser, {}, function() {
|
||||
content.wrappedJSObject.firstCall();
|
||||
});
|
||||
await onFirstCallFramesAdded;
|
||||
const stackFrames = dbg.selectors.getCallStackFrames(dbg.getState());
|
||||
|
||||
info("frames added, select the console again");
|
||||
await openConsole();
|
||||
|
@ -62,7 +62,9 @@ add_task(async function() {
|
|||
|
||||
info("select the debugger and select the frame (1)");
|
||||
await openDebugger();
|
||||
stackFrames.selectFrame(1);
|
||||
|
||||
await dbg.actions.selectFrame(stackFrames[1]);
|
||||
|
||||
await openConsole();
|
||||
|
||||
info("Check `foo + foo2 + foo3` value when paused on a given frame");
|
||||
|
@ -82,3 +84,11 @@ add_task(async function() {
|
|||
ok(!content.wrappedJSObject.foo3, "`foo3` was not added to the content window");
|
||||
});
|
||||
});
|
||||
|
||||
async function pauseDebugger(dbg) {
|
||||
info("Waiting for debugger to pause");
|
||||
ContentTask.spawn(gBrowser.selectedBrowser, {}, async function() {
|
||||
content.wrappedJSObject.firstCall();
|
||||
});
|
||||
await waitForPaused(dbg);
|
||||
}
|
||||
|
|
|
@ -9,22 +9,26 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
// Import helpers for the new debugger
|
||||
/* import-globals-from ../../../debugger/new/test/mochitest/helpers.js */
|
||||
Services.scriptloader.loadSubScript(
|
||||
"chrome://mochitests/content/browser/devtools/client/debugger/new/test/mochitest/helpers.js",
|
||||
this);
|
||||
|
||||
const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
|
||||
"test/mochitest/test-eval-in-stackframe.html";
|
||||
|
||||
add_task(async function() {
|
||||
// Force the old debugger UI since it's directly used (see Bug 1301705).
|
||||
await pushPref("devtools.debugger.new-debugger-frontend", false);
|
||||
|
||||
info("open the console");
|
||||
const hud = await openNewTabAndConsole(TEST_URI);
|
||||
const {jsterm} = hud;
|
||||
|
||||
info("open the debugger");
|
||||
const {panel} = await openDebugger();
|
||||
const {activeThread} = panel.panelWin.DebuggerController;
|
||||
await openDebugger();
|
||||
|
||||
const toolbox = gDevTools.getToolbox(hud.target);
|
||||
const dbg = createDebuggerContext(toolbox);
|
||||
|
||||
const onFirstCallFramesAdded = activeThread.addOneTimeListener("framesadded");
|
||||
// firstCall calls secondCall, which has a debugger statement, so we'll be paused.
|
||||
const onFirstCallMessageReceived = waitForMessage(hud, "undefined");
|
||||
|
||||
|
@ -36,7 +40,7 @@ add_task(async function() {
|
|||
jsterm.execute("firstCall()");
|
||||
|
||||
info("Waiting for a frame to be added");
|
||||
await onFirstCallFramesAdded;
|
||||
await waitForPaused(dbg);
|
||||
|
||||
info("frames added, select the console again");
|
||||
await openConsole();
|
||||
|
@ -57,7 +61,7 @@ add_task(async function() {
|
|||
ok(firstCallEvaluationResult === unresolvedSymbol, "firstCall was not evaluated yet");
|
||||
|
||||
info("Resuming the thread");
|
||||
activeThread.resume();
|
||||
dbg.actions.resume(dbg.getState());
|
||||
|
||||
message = await onFirstCallMessageReceived;
|
||||
ok(firstCallEvaluationResult !== unresolvedSymbol,
|
||||
|
|
|
@ -8,6 +8,12 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
// Import helpers for the new debugger
|
||||
/* import-globals-from ../../../debugger/new/test/mochitest/helpers.js */
|
||||
Services.scriptloader.loadSubScript(
|
||||
"chrome://mochitests/content/browser/devtools/client/debugger/new/test/mochitest/helpers.js",
|
||||
this);
|
||||
|
||||
const TEST_URI = "https://example.com/browser/devtools/client/webconsole/" +
|
||||
"test/mochitest/test-eval-in-stackframe.html";
|
||||
|
||||
|
|
|
@ -8,32 +8,28 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
// Import helpers for the new debugger
|
||||
/* import-globals-from ../../../debugger/new/test/mochitest/helpers.js */
|
||||
Services.scriptloader.loadSubScript(
|
||||
"chrome://mochitests/content/browser/devtools/client/debugger/new/test/mochitest/helpers.js",
|
||||
this);
|
||||
|
||||
const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
|
||||
"test/mochitest/" +
|
||||
"test-closure-optimized-out.html";
|
||||
|
||||
add_task(async function() {
|
||||
// Force the old debugger UI since it's directly used (see Bug 1301705)
|
||||
await pushPref("devtools.debugger.new-debugger-frontend", false);
|
||||
|
||||
const hud = await openNewTabAndConsole(TEST_URI);
|
||||
const { toolbox, panel: debuggerPanel } = await openDebugger();
|
||||
await openDebugger();
|
||||
|
||||
const sources = debuggerPanel.panelWin.DebuggerView.Sources;
|
||||
await debuggerPanel.addBreakpoint({ actor: sources.values[0], line: 18 });
|
||||
await ensureThreadClientState(debuggerPanel, "resumed");
|
||||
const toolbox = gDevTools.getToolbox(hud.target);
|
||||
const dbg = createDebuggerContext(toolbox);
|
||||
|
||||
const { FETCHED_SCOPES } = debuggerPanel.panelWin.EVENTS;
|
||||
const fetchedScopes = debuggerPanel.panelWin.once(FETCHED_SCOPES);
|
||||
await addBreakpoint(dbg, "test-closure-optimized-out.html", 18);
|
||||
await waitForThreadEvents(dbg, "resumed");
|
||||
|
||||
// Cause the debuggee to pause
|
||||
ContentTask.spawn(gBrowser.selectedBrowser, {}, async function() {
|
||||
const button = content.document.querySelector("button");
|
||||
button.click();
|
||||
});
|
||||
|
||||
await fetchedScopes;
|
||||
ok(true, "Scopes were fetched");
|
||||
await pauseDebugger(dbg);
|
||||
|
||||
await toolbox.selectTool("webconsole");
|
||||
|
||||
|
@ -45,15 +41,23 @@ add_task(async function() {
|
|||
await onMessage;
|
||||
|
||||
ok(true, "Optimized out message logged");
|
||||
|
||||
info("Open the debugger");
|
||||
await openDebugger();
|
||||
|
||||
info("Resume");
|
||||
await resume(dbg);
|
||||
|
||||
info("Remove the breakpoint");
|
||||
const source = findSource(dbg, "test-closure-optimized-out.html");
|
||||
await removeBreakpoint(dbg, source.id, 18);
|
||||
});
|
||||
|
||||
// Debugger helper functions adapted from devtools/client/debugger/test/head.js.
|
||||
|
||||
async function ensureThreadClientState(debuggerPanel, state) {
|
||||
const thread = debuggerPanel.panelWin.gThreadClient;
|
||||
info(`Thread is: '${thread.state}'.`);
|
||||
if (thread.state != state) {
|
||||
info("Waiting for thread event: '${state}'.");
|
||||
await thread.addOneTimeListener(state);
|
||||
}
|
||||
async function pauseDebugger(dbg) {
|
||||
info("Waiting for debugger to pause");
|
||||
ContentTask.spawn(gBrowser.selectedBrowser, {}, async function() {
|
||||
const button = content.document.querySelector("button");
|
||||
button.click();
|
||||
});
|
||||
await waitForPaused(dbg);
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче