mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
DEV: Remove more legacy ember code (#17218)
Also fixes flaky d-editor tests
This commit is contained in:
parent
f73796b258
commit
9669794f85
@ -1,48 +1,12 @@
|
|||||||
/* global andThen */
|
import { render } from "@ember/test-helpers";
|
||||||
|
|
||||||
import { TestModuleForComponent, render } from "@ember/test-helpers";
|
|
||||||
import MessageBus from "message-bus-client";
|
|
||||||
import EmberObject from "@ember/object";
|
|
||||||
import { setupRenderingTest as EmberSetupRenderingTest } from "ember-qunit";
|
|
||||||
import Session from "discourse/models/session";
|
import Session from "discourse/models/session";
|
||||||
import Site from "discourse/models/site";
|
import Site from "discourse/models/site";
|
||||||
import TopicTrackingState from "discourse/models/topic-tracking-state";
|
import TopicTrackingState from "discourse/models/topic-tracking-state";
|
||||||
import User from "discourse/models/user";
|
import User from "discourse/models/user";
|
||||||
import { autoLoadModules } from "discourse/initializers/auto-load-modules";
|
import { autoLoadModules } from "discourse/initializers/auto-load-modules";
|
||||||
import createStore from "discourse/tests/helpers/create-store";
|
|
||||||
import { currentSettings } from "discourse/tests/helpers/site-settings";
|
|
||||||
import QUnit, { test } from "qunit";
|
import QUnit, { test } from "qunit";
|
||||||
import KeyValueStore from "discourse/lib/key-value-store";
|
|
||||||
|
|
||||||
const LEGACY_ENV = !EmberSetupRenderingTest;
|
export { setupRenderingTest } from "ember-qunit";
|
||||||
|
|
||||||
export function setupRenderingTest(hooks) {
|
|
||||||
if (!LEGACY_ENV) {
|
|
||||||
return EmberSetupRenderingTest.apply(this, arguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
let testModule;
|
|
||||||
|
|
||||||
hooks.before(function () {
|
|
||||||
const name = this.moduleName.split("|").pop();
|
|
||||||
testModule = new TestModuleForComponent(name, {
|
|
||||||
integration: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
hooks.beforeEach(function () {
|
|
||||||
testModule.setContext(this);
|
|
||||||
return testModule.setup(...arguments);
|
|
||||||
});
|
|
||||||
|
|
||||||
hooks.afterEach(function () {
|
|
||||||
return testModule.teardown(...arguments);
|
|
||||||
});
|
|
||||||
|
|
||||||
hooks.after(function () {
|
|
||||||
testModule = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function (name, opts) {
|
export default function (name, opts) {
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
@ -51,7 +15,7 @@ export default function (name, opts) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof opts.template === "string" && !LEGACY_ENV) {
|
if (typeof opts.template === "string") {
|
||||||
let testName = QUnit.config.currentModule.name + " " + name;
|
let testName = QUnit.config.currentModule.name + " " + name;
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
console.warn(
|
console.warn(
|
||||||
@ -63,61 +27,10 @@ export default function (name, opts) {
|
|||||||
test(name, async function (assert) {
|
test(name, async function (assert) {
|
||||||
this.site = Site.current();
|
this.site = Site.current();
|
||||||
this.session = Session.current();
|
this.session = Session.current();
|
||||||
|
this.container = this.owner;
|
||||||
|
const store = this.owner.lookup("service:store");
|
||||||
|
|
||||||
let owner = LEGACY_ENV ? this.registry : this.owner;
|
autoLoadModules(this.owner, this.registry);
|
||||||
let store;
|
|
||||||
|
|
||||||
if (LEGACY_ENV) {
|
|
||||||
this.registry.register("site-settings:main", currentSettings(), {
|
|
||||||
instantiate: false,
|
|
||||||
});
|
|
||||||
this.registry.register("capabilities:main", EmberObject);
|
|
||||||
this.registry.register("message-bus:main", MessageBus, {
|
|
||||||
instantiate: false,
|
|
||||||
});
|
|
||||||
this.registry.register("site:main", this.site, { instantiate: false });
|
|
||||||
this.registry.register("session:main", this.session, {
|
|
||||||
instantiate: false,
|
|
||||||
});
|
|
||||||
const keyValueStore = new KeyValueStore("discourse_");
|
|
||||||
this.registry.register("key-value-store:main", keyValueStore, {
|
|
||||||
instantiate: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.registry.injection(
|
|
||||||
"component",
|
|
||||||
"siteSettings",
|
|
||||||
"site-settings:main"
|
|
||||||
);
|
|
||||||
this.registry.injection("component", "appEvents", "service:app-events");
|
|
||||||
this.registry.injection("component", "capabilities", "capabilities:main");
|
|
||||||
this.registry.injection("component", "site", "site:main");
|
|
||||||
this.registry.injection("component", "session", "session:main");
|
|
||||||
this.registry.injection("component", "messageBus", "message-bus:main");
|
|
||||||
this.registry.injection(
|
|
||||||
"component",
|
|
||||||
"keyValueStore",
|
|
||||||
"key-value-store:main"
|
|
||||||
);
|
|
||||||
|
|
||||||
this.registry.injection("service", "session", "session:main");
|
|
||||||
this.registry.injection("service", "messageBus", "message-bus:main");
|
|
||||||
this.registry.injection("service", "siteSettings", "site-settings:main");
|
|
||||||
this.registry.injection(
|
|
||||||
"service",
|
|
||||||
"keyValueStore",
|
|
||||||
"key-value-store:main"
|
|
||||||
);
|
|
||||||
|
|
||||||
this.siteSettings = currentSettings();
|
|
||||||
store = createStore();
|
|
||||||
this.registry.register("service:store", store, { instantiate: false });
|
|
||||||
} else {
|
|
||||||
this.container = owner;
|
|
||||||
store = this.container.lookup("service:store");
|
|
||||||
}
|
|
||||||
|
|
||||||
autoLoadModules(this.container, this.registry);
|
|
||||||
|
|
||||||
if (!opts.anonymous) {
|
if (!opts.anonymous) {
|
||||||
const currentUser = User.create({
|
const currentUser = User.create({
|
||||||
@ -126,58 +39,41 @@ export default function (name, opts) {
|
|||||||
});
|
});
|
||||||
this.currentUser = currentUser;
|
this.currentUser = currentUser;
|
||||||
|
|
||||||
owner.unregister("current-user:main");
|
this.owner.unregister("current-user:main");
|
||||||
owner.register("current-user:main", currentUser, {
|
this.owner.register("current-user:main", currentUser, {
|
||||||
instantiate: false,
|
instantiate: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (LEGACY_ENV) {
|
this.owner.inject("component", "currentUser", "current-user:main");
|
||||||
owner.injection("component", "currentUser", "current-user:main");
|
this.owner.inject("service", "currentUser", "current-user:main");
|
||||||
owner.injection("service", "currentUser", "current-user:main");
|
|
||||||
} else {
|
|
||||||
owner.inject("component", "currentUser", "current-user:main");
|
|
||||||
owner.inject("service", "currentUser", "current-user:main");
|
|
||||||
}
|
|
||||||
|
|
||||||
owner.unregister("topic-tracking-state:main");
|
this.owner.unregister("topic-tracking-state:main");
|
||||||
owner.register(
|
this.owner.register(
|
||||||
"topic-tracking-state:main",
|
"topic-tracking-state:main",
|
||||||
TopicTrackingState.create({ currentUser }),
|
TopicTrackingState.create({ currentUser }),
|
||||||
{ instantiate: false }
|
{ instantiate: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
if (LEGACY_ENV) {
|
this.owner.inject(
|
||||||
owner.injection(
|
"service",
|
||||||
"service",
|
"topicTrackingState",
|
||||||
"topicTrackingState",
|
"topic-tracking-state:main"
|
||||||
"topic-tracking-state:main"
|
);
|
||||||
);
|
|
||||||
} else {
|
|
||||||
owner.inject(
|
|
||||||
"service",
|
|
||||||
"topicTrackingState",
|
|
||||||
"topic-tracking-state:main"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.beforeEach) {
|
if (opts.beforeEach) {
|
||||||
opts.beforeEach.call(this, store);
|
await opts.beforeEach.call(this, store);
|
||||||
}
|
}
|
||||||
|
|
||||||
$.fn.autocomplete = function () {};
|
$.fn.autocomplete = function () {};
|
||||||
andThen(() => {
|
|
||||||
return LEGACY_ENV ? this.render(opts.template) : render(opts.template);
|
|
||||||
});
|
|
||||||
|
|
||||||
andThen(() => {
|
try {
|
||||||
return opts.test.call(this, assert);
|
await render(opts.template);
|
||||||
}).finally(async () => {
|
await opts.test.call(this, assert);
|
||||||
|
} finally {
|
||||||
if (opts.afterEach) {
|
if (opts.afterEach) {
|
||||||
await andThen(() => {
|
await opts.afterEach.call(opts);
|
||||||
return opts.afterEach.call(opts);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -70,8 +70,6 @@ import {
|
|||||||
clearTextDecorateCallbacks,
|
clearTextDecorateCallbacks,
|
||||||
} from "discourse/lib/to-markdown";
|
} from "discourse/lib/to-markdown";
|
||||||
|
|
||||||
const LEGACY_ENV = !setupApplicationTest;
|
|
||||||
|
|
||||||
export function currentUser() {
|
export function currentUser() {
|
||||||
return User.create(sessionFixtures["/session/current.json"].current_user);
|
return User.create(sessionFixtures["/session/current.json"].current_user);
|
||||||
}
|
}
|
||||||
@ -188,9 +186,7 @@ function testCleanup(container, app) {
|
|||||||
resetLastEditNotificationClick();
|
resetLastEditNotificationClick();
|
||||||
clearAuthMethods();
|
clearAuthMethods();
|
||||||
setTestPresence(true);
|
setTestPresence(true);
|
||||||
if (!LEGACY_ENV) {
|
clearPresenceCallbacks();
|
||||||
clearPresenceCallbacks();
|
|
||||||
}
|
|
||||||
restoreBaseUri();
|
restoreBaseUri();
|
||||||
resetTopicsSectionLinks();
|
resetTopicsSectionLinks();
|
||||||
clearTagDecorateCallbacks();
|
clearTagDecorateCallbacks();
|
||||||
@ -217,9 +213,7 @@ export function discourseModule(name, options) {
|
|||||||
|
|
||||||
this.getController = function (controllerName, properties) {
|
this.getController = function (controllerName, properties) {
|
||||||
let controller = this.container.lookup(`controller:${controllerName}`);
|
let controller = this.container.lookup(`controller:${controllerName}`);
|
||||||
if (!LEGACY_ENV) {
|
controller.application = {};
|
||||||
controller.application = {};
|
|
||||||
}
|
|
||||||
controller.siteSettings = this.siteSettings;
|
controller.siteSettings = this.siteSettings;
|
||||||
if (properties) {
|
if (properties) {
|
||||||
controller.setProperties(properties);
|
controller.setProperties(properties);
|
||||||
@ -304,16 +298,7 @@ export function acceptance(name, optionsOrCallback) {
|
|||||||
|
|
||||||
resetSite(currentSettings(), siteChanges);
|
resetSite(currentSettings(), siteChanges);
|
||||||
|
|
||||||
if (LEGACY_ENV) {
|
|
||||||
getApplication().__registeredObjects__ = false;
|
|
||||||
getApplication().reset();
|
|
||||||
}
|
|
||||||
this.container = getOwner(this);
|
this.container = getOwner(this);
|
||||||
if (LEGACY_ENV && loggedIn) {
|
|
||||||
updateCurrentUser({
|
|
||||||
appEvents: this.container.lookup("service:app-events"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.owner) {
|
if (!this.owner) {
|
||||||
this.owner = this.container;
|
this.owner = this.container;
|
||||||
@ -330,11 +315,6 @@ export function acceptance(name, optionsOrCallback) {
|
|||||||
options?.afterEach?.call(this);
|
options?.afterEach?.call(this);
|
||||||
testCleanup(this.container, app);
|
testCleanup(this.container, app);
|
||||||
|
|
||||||
if (LEGACY_ENV) {
|
|
||||||
app.__registeredObjects__ = false;
|
|
||||||
app.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
// We do this after reset so that the willClearRender will have already fired
|
// We do this after reset so that the willClearRender will have already fired
|
||||||
resetWidgetCleanCallbacks();
|
resetWidgetCleanCallbacks();
|
||||||
},
|
},
|
||||||
@ -380,18 +360,16 @@ export function acceptance(name, optionsOrCallback) {
|
|||||||
hooks.afterEach(setup.afterEach);
|
hooks.afterEach(setup.afterEach);
|
||||||
callback(needs);
|
callback(needs);
|
||||||
|
|
||||||
if (!LEGACY_ENV && getContext) {
|
setupApplicationTest(hooks);
|
||||||
setupApplicationTest(hooks);
|
|
||||||
|
|
||||||
hooks.beforeEach(function () {
|
hooks.beforeEach(function () {
|
||||||
// This hack seems necessary to allow `DiscourseURL` to use the testing router
|
// This hack seems necessary to allow `DiscourseURL` to use the testing router
|
||||||
let ctx = getContext();
|
let ctx = getContext();
|
||||||
this.container.registry.unregister("router:main");
|
this.container.registry.unregister("router:main");
|
||||||
this.container.registry.register("router:main", ctx.owner.router, {
|
this.container.registry.register("router:main", ctx.owner.router, {
|
||||||
instantiate: false,
|
instantiate: false,
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Old way
|
// Old way
|
||||||
@ -523,25 +501,7 @@ export async function selectText(selector, endOffset = null) {
|
|||||||
selection.addRange(range);
|
selection.addRange(range);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (LEGACY_ENV) {
|
performSelection();
|
||||||
// In the Ember CLI environment, the settled() helper seems to take care of waiting
|
|
||||||
// for this event to fire. In legacy, we need to do it manually.
|
|
||||||
let callback;
|
|
||||||
const selectEventFiredPromise = new Promise((resolve) => {
|
|
||||||
callback = resolve;
|
|
||||||
document.addEventListener("selectionchange", callback);
|
|
||||||
});
|
|
||||||
|
|
||||||
performSelection();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await selectEventFiredPromise;
|
|
||||||
} finally {
|
|
||||||
document.removeEventListener("selectionchange", callback);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
performSelection();
|
|
||||||
}
|
|
||||||
|
|
||||||
await settled();
|
await settled();
|
||||||
}
|
}
|
||||||
|
@ -98,9 +98,9 @@ discourseModule("Integration | Component | d-editor", function (hooks) {
|
|||||||
beforeEach() {
|
beforeEach() {
|
||||||
this.set("value", "hello world.");
|
this.set("value", "hello world.");
|
||||||
},
|
},
|
||||||
test(assert) {
|
async test(assert) {
|
||||||
const textarea = jumpEnd(query("textarea.d-editor-input"));
|
const textarea = jumpEnd(query("textarea.d-editor-input"));
|
||||||
testFunc.call(this, assert, textarea);
|
await testFunc.call(this, assert, textarea);
|
||||||
},
|
},
|
||||||
skip: !navigator.userAgent.includes("Chrome"),
|
skip: !navigator.userAgent.includes("Chrome"),
|
||||||
});
|
});
|
||||||
@ -113,9 +113,9 @@ discourseModule("Integration | Component | d-editor", function (hooks) {
|
|||||||
this.set("value", "hello world.");
|
this.set("value", "hello world.");
|
||||||
},
|
},
|
||||||
|
|
||||||
test(assert) {
|
async test(assert) {
|
||||||
const textarea = jumpEnd(query("textarea.d-editor-input"));
|
const textarea = jumpEnd(query("textarea.d-editor-input"));
|
||||||
testFunc.call(this, assert, textarea);
|
await testFunc.call(this, assert, textarea);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -10,10 +10,7 @@ import pretender, {
|
|||||||
pretenderHelpers,
|
pretenderHelpers,
|
||||||
resetPretender,
|
resetPretender,
|
||||||
} from "discourse/tests/helpers/create-pretender";
|
} from "discourse/tests/helpers/create-pretender";
|
||||||
import {
|
import { resetSettings } from "discourse/tests/helpers/site-settings";
|
||||||
currentSettings,
|
|
||||||
resetSettings,
|
|
||||||
} from "discourse/tests/helpers/site-settings";
|
|
||||||
import { setDefaultOwner } from "discourse-common/lib/get-owner";
|
import { setDefaultOwner } from "discourse-common/lib/get-owner";
|
||||||
import { setApplication, setResolver } from "@ember/test-helpers";
|
import { setApplication, setResolver } from "@ember/test-helpers";
|
||||||
import { setupS3CDN, setupURL } from "discourse-common/lib/get-url";
|
import { setupS3CDN, setupURL } from "discourse-common/lib/get-url";
|
||||||
@ -394,20 +391,6 @@ function setupTestsCommon(application, container, config) {
|
|||||||
resetSite();
|
resetSite();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setupTestsLegacy(application) {
|
|
||||||
app = application;
|
|
||||||
setResolver(buildResolver("discourse").create({ namespace: app }));
|
|
||||||
setupTestsCommon(application, app.__container__);
|
|
||||||
|
|
||||||
app.instanceInitializer({
|
|
||||||
name: "test-helper",
|
|
||||||
initialize: testsInitialized,
|
|
||||||
teardown: testsTornDown,
|
|
||||||
});
|
|
||||||
app.SiteSettings = currentSettings();
|
|
||||||
app.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function setupTests(config) {
|
export default function setupTests(config) {
|
||||||
let settings = resetSettings();
|
let settings = resetSettings();
|
||||||
app = createApplication(config, settings);
|
app = createApplication(config, settings);
|
||||||
|
@ -1,115 +0,0 @@
|
|||||||
// discourse-skip-module
|
|
||||||
|
|
||||||
document.body.insertAdjacentHTML(
|
|
||||||
"afterbegin",
|
|
||||||
`
|
|
||||||
<div id="ember-testing-container"><div id="ember-testing"></div></div>
|
|
||||||
<style>#ember-testing-container { position: fixed; background: white; bottom: 0; right: 0; width: 640px; height: 384px; overflow: auto; z-index: 9999; border: 1px solid #ccc; transform: translateZ(0)} #ember-testing { width: 200%; height: 200%; transform: scale(0.5); transform-origin: top left; }</style>
|
|
||||||
`
|
|
||||||
);
|
|
||||||
|
|
||||||
let setupTestsLegacy = require("discourse/tests/setup-tests").setupTestsLegacy;
|
|
||||||
setupTestsLegacy(window.Discourse);
|
|
||||||
|
|
||||||
const keyFromKeyCode = {
|
|
||||||
8: "Backspace",
|
|
||||||
9: "Tab",
|
|
||||||
13: "Enter",
|
|
||||||
16: "Shift",
|
|
||||||
17: "Control",
|
|
||||||
18: "Alt",
|
|
||||||
20: "CapsLock",
|
|
||||||
27: "Escape",
|
|
||||||
32: " ",
|
|
||||||
37: "ArrowLeft",
|
|
||||||
38: "ArrowUp",
|
|
||||||
39: "ArrowRight",
|
|
||||||
40: "ArrowDown",
|
|
||||||
48: "0",
|
|
||||||
49: "1",
|
|
||||||
50: "2",
|
|
||||||
51: "3",
|
|
||||||
52: "4",
|
|
||||||
53: "5",
|
|
||||||
54: "6",
|
|
||||||
55: "7",
|
|
||||||
56: "8",
|
|
||||||
57: "9",
|
|
||||||
65: "a",
|
|
||||||
66: "b",
|
|
||||||
67: "c",
|
|
||||||
68: "d",
|
|
||||||
69: "e",
|
|
||||||
70: "f",
|
|
||||||
71: "g",
|
|
||||||
72: "h",
|
|
||||||
73: "i",
|
|
||||||
74: "j",
|
|
||||||
75: "k",
|
|
||||||
76: "l",
|
|
||||||
77: "m",
|
|
||||||
78: "n",
|
|
||||||
79: "o",
|
|
||||||
80: "p",
|
|
||||||
81: "q",
|
|
||||||
82: "r",
|
|
||||||
83: "s",
|
|
||||||
84: "t",
|
|
||||||
85: "u",
|
|
||||||
86: "v",
|
|
||||||
87: "w",
|
|
||||||
88: "x",
|
|
||||||
89: "y",
|
|
||||||
90: "z",
|
|
||||||
91: "Meta",
|
|
||||||
93: "Meta",
|
|
||||||
97: "a",
|
|
||||||
98: "b",
|
|
||||||
99: "c",
|
|
||||||
100: "d",
|
|
||||||
101: "e",
|
|
||||||
102: "f",
|
|
||||||
103: "g",
|
|
||||||
104: "h",
|
|
||||||
105: "i",
|
|
||||||
106: "j",
|
|
||||||
107: "k",
|
|
||||||
108: "l",
|
|
||||||
109: "m",
|
|
||||||
110: "n",
|
|
||||||
111: "o",
|
|
||||||
112: "p",
|
|
||||||
113: "q",
|
|
||||||
114: "r",
|
|
||||||
115: "s",
|
|
||||||
116: "t",
|
|
||||||
117: "u",
|
|
||||||
118: "v",
|
|
||||||
119: "w",
|
|
||||||
120: "x",
|
|
||||||
121: "y",
|
|
||||||
122: "z",
|
|
||||||
187: "=",
|
|
||||||
189: "-",
|
|
||||||
};
|
|
||||||
|
|
||||||
window.keyEvent = function (selector, contextOrType, typeOrKeyCode, keyCode) {
|
|
||||||
let context, type;
|
|
||||||
|
|
||||||
if (keyCode === undefined) {
|
|
||||||
context = null;
|
|
||||||
keyCode = typeOrKeyCode;
|
|
||||||
type = contextOrType;
|
|
||||||
} else {
|
|
||||||
context = contextOrType;
|
|
||||||
type = typeOrKeyCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
let key = keyFromKeyCode[keyCode];
|
|
||||||
|
|
||||||
return window.triggerEvent(selector, context, type, {
|
|
||||||
keyCode,
|
|
||||||
which: keyCode,
|
|
||||||
key,
|
|
||||||
});
|
|
||||||
};
|
|
@ -6,7 +6,6 @@
|
|||||||
"author": "Discourse",
|
"author": "Discourse",
|
||||||
"license": "GPL-2.0-only",
|
"license": "GPL-2.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "5.15.4",
|
|
||||||
"@discourse/moment-timezone-names-translations": "^1.0.0",
|
"@discourse/moment-timezone-names-translations": "^1.0.0",
|
||||||
"@fortawesome/fontawesome-free": "5.15.4",
|
"@fortawesome/fontawesome-free": "5.15.4",
|
||||||
"@highlightjs/cdn-assets": "^10.7.0",
|
"@highlightjs/cdn-assets": "^10.7.0",
|
||||||
|
Loading…
Reference in New Issue
Block a user