mirror of
https://github.com/discourse/discourse.git
synced 2025-04-19 17:59:05 +08:00

* FEATURE: reduce avatar sizes to 6 from 20 This PR introduces 3 changes: 1. SiteSetting.avatar_sizes, now does what is says on the tin. previously it would introduce a large number of extra sizes, to allow for various DPIs. Instead we now trust the admin with the size list. 2. When `avatar_sizes` changes, we ensure consistency and remove resized avatars that are not longer allowed per site setting. This happens on the 12 hourly job and limited out of the box to 20k cleanups per cycle, given this may reach out to AWS 20k times to remove things. 3.Our default avatar sizes are now "24|48|72|96|144|288" these sizes were very specifically picked to limit amount of bluriness introduced by webkit. Our avatars are already blurry due to 1px border, so this corrects old blur. This change heavily reduces storage required by forums which simplifies site moves and more. Co-authored-by: David Taylor <david@taylorhq.com>
461 lines
13 KiB
JavaScript
461 lines
13 KiB
JavaScript
import {
|
|
avatarImg,
|
|
avatarUrl,
|
|
caretRowCol,
|
|
clipboardCopyAsync,
|
|
defaultHomepage,
|
|
emailValid,
|
|
escapeExpression,
|
|
extractDomainFromUrl,
|
|
fillMissingDates,
|
|
getRawAvatarSize,
|
|
inCodeBlock,
|
|
initializeDefaultHomepage,
|
|
mergeSortedLists,
|
|
modKeysPressed,
|
|
setCaretPosition,
|
|
setDefaultHomepage,
|
|
slugify,
|
|
toAsciiPrintable,
|
|
} from "discourse/lib/utilities";
|
|
import sinon from "sinon";
|
|
import { module, test } from "qunit";
|
|
import Handlebars from "handlebars";
|
|
import { chromeTest } from "discourse/tests/helpers/qunit-helpers";
|
|
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
|
import { click, render } from "@ember/test-helpers";
|
|
import { hbs } from "ember-cli-htmlbars";
|
|
import { setupURL } from "discourse-common/lib/get-url";
|
|
import { setupTest } from "ember-qunit";
|
|
import { getOwner } from "discourse-common/lib/get-owner";
|
|
|
|
module("Unit | Utilities", function (hooks) {
|
|
setupTest(hooks);
|
|
|
|
test("escapeExpression", function (assert) {
|
|
assert.strictEqual(
|
|
escapeExpression(">"),
|
|
">",
|
|
"escapes unsafe characters"
|
|
);
|
|
|
|
assert.strictEqual(
|
|
escapeExpression(new Handlebars.SafeString(">")),
|
|
">",
|
|
"does not double-escape safe strings"
|
|
);
|
|
|
|
assert.strictEqual(
|
|
escapeExpression(undefined),
|
|
"",
|
|
"returns a falsy string when given a falsy value"
|
|
);
|
|
});
|
|
|
|
test("emailValid", function (assert) {
|
|
assert.ok(
|
|
emailValid("Bob@example.com"),
|
|
"allows upper case in the first part of emails"
|
|
);
|
|
assert.ok(
|
|
emailValid("bob@EXAMPLE.com"),
|
|
"allows upper case in the email domain"
|
|
);
|
|
});
|
|
|
|
test("extractDomainFromUrl", function (assert) {
|
|
assert.strictEqual(
|
|
extractDomainFromUrl("http://meta.discourse.org:443/random"),
|
|
"meta.discourse.org",
|
|
"extract domain name from url"
|
|
);
|
|
assert.strictEqual(
|
|
extractDomainFromUrl("meta.discourse.org:443/random"),
|
|
"meta.discourse.org",
|
|
"extract domain regardless of scheme presence"
|
|
);
|
|
assert.strictEqual(
|
|
extractDomainFromUrl("http://192.168.0.1:443/random"),
|
|
"192.168.0.1",
|
|
"works for IP address"
|
|
);
|
|
assert.strictEqual(
|
|
extractDomainFromUrl("http://localhost:443/random"),
|
|
"localhost",
|
|
"works for localhost"
|
|
);
|
|
});
|
|
|
|
test("getRawAvatarSize avoids redirects", function (assert) {
|
|
assert.strictEqual(
|
|
getRawAvatarSize(1),
|
|
24,
|
|
"returns the first size larger on the menu"
|
|
);
|
|
|
|
assert.strictEqual(getRawAvatarSize(2000), 288, "caps at highest");
|
|
});
|
|
|
|
test("avatarUrl", function (assert) {
|
|
assert.blank(avatarUrl("", "tiny"), "no template returns blank");
|
|
assert.strictEqual(
|
|
avatarUrl("/fake/template/{size}.png", "tiny"),
|
|
"/fake/template/" + getRawAvatarSize(24) + ".png",
|
|
"simple avatar url"
|
|
);
|
|
assert.strictEqual(
|
|
avatarUrl("/fake/template/{size}.png", "large"),
|
|
"/fake/template/" + getRawAvatarSize(48) + ".png",
|
|
"different size"
|
|
);
|
|
|
|
setupURL("https://app-cdn.example.com", "https://example.com", "");
|
|
|
|
assert.strictEqual(
|
|
avatarUrl("/fake/template/{size}.png", "large"),
|
|
"https://app-cdn.example.com/fake/template/" +
|
|
getRawAvatarSize(48) +
|
|
".png",
|
|
"uses CDN if present"
|
|
);
|
|
});
|
|
|
|
let setDevicePixelRatio = function (value) {
|
|
if (Object.defineProperty && !window.hasOwnProperty("devicePixelRatio")) {
|
|
Object.defineProperty(window, "devicePixelRatio", { value: 2 });
|
|
} else {
|
|
window.devicePixelRatio = value;
|
|
}
|
|
};
|
|
|
|
test("avatarImg", function (assert) {
|
|
let oldRatio = window.devicePixelRatio;
|
|
setDevicePixelRatio(2);
|
|
|
|
let avatarTemplate = "/path/to/avatar/{size}.png";
|
|
assert.strictEqual(
|
|
avatarImg({ avatarTemplate, size: "tiny" }),
|
|
"<img loading='lazy' alt='' width='24' height='24' src='/path/to/avatar/48.png' class='avatar'>",
|
|
"it returns the avatar html"
|
|
);
|
|
|
|
assert.strictEqual(
|
|
avatarImg({
|
|
avatarTemplate,
|
|
size: "tiny",
|
|
title: "evilest trout",
|
|
}),
|
|
"<img loading='lazy' alt='' width='24' height='24' src='/path/to/avatar/48.png' class='avatar' title='evilest trout' aria-label='evilest trout'>",
|
|
"it adds a title if supplied"
|
|
);
|
|
|
|
assert.strictEqual(
|
|
avatarImg({
|
|
avatarTemplate,
|
|
size: "tiny",
|
|
extraClasses: "evil fish",
|
|
}),
|
|
"<img loading='lazy' alt='' width='24' height='24' src='/path/to/avatar/48.png' class='avatar evil fish'>",
|
|
"it adds extra classes if supplied"
|
|
);
|
|
|
|
assert.blank(
|
|
avatarImg({ avatarTemplate: "", size: "tiny" }),
|
|
"it doesn't render avatars for invalid avatar template"
|
|
);
|
|
|
|
setDevicePixelRatio(oldRatio);
|
|
});
|
|
|
|
test("defaultHomepage via meta tag", function (assert) {
|
|
let meta = document.createElement("meta");
|
|
meta.name = "discourse_current_homepage";
|
|
meta.content = "hot";
|
|
document.body.appendChild(meta);
|
|
|
|
const siteSettings = getOwner(this).lookup("service:site-settings");
|
|
initializeDefaultHomepage(siteSettings);
|
|
|
|
assert.strictEqual(
|
|
defaultHomepage(),
|
|
"hot",
|
|
"default homepage is pulled from <meta name=discourse_current_homepage>"
|
|
);
|
|
document.body.removeChild(meta);
|
|
});
|
|
|
|
test("defaultHomepage via site settings", function (assert) {
|
|
const siteSettings = getOwner(this).lookup("service:site-settings");
|
|
siteSettings.top_menu = "top|latest|hot";
|
|
initializeDefaultHomepage(siteSettings);
|
|
|
|
assert.strictEqual(
|
|
defaultHomepage(),
|
|
"top",
|
|
"default homepage is the first item in the top_menu site setting"
|
|
);
|
|
});
|
|
|
|
test("setDefaultHomepage", function (assert) {
|
|
const siteSettings = getOwner(this).lookup("service:site-settings");
|
|
initializeDefaultHomepage(siteSettings);
|
|
|
|
assert.strictEqual(defaultHomepage(), "latest");
|
|
|
|
setDefaultHomepage("top");
|
|
assert.strictEqual(defaultHomepage(), "top");
|
|
});
|
|
|
|
test("caretRowCol", function (assert) {
|
|
let textarea = document.createElement("textarea");
|
|
const content = document.createTextNode("01234\n56789\n012345");
|
|
textarea.appendChild(content);
|
|
document.body.appendChild(textarea);
|
|
|
|
const assertResult = (setCaretPos, expectedRowNum, expectedColNum) => {
|
|
setCaretPosition(textarea, setCaretPos);
|
|
|
|
const result = caretRowCol(textarea);
|
|
assert.strictEqual(
|
|
result.rowNum,
|
|
expectedRowNum,
|
|
"returns the right row of the caret"
|
|
);
|
|
assert.strictEqual(
|
|
result.colNum,
|
|
expectedColNum,
|
|
"returns the right col of the caret"
|
|
);
|
|
};
|
|
|
|
assertResult(0, 1, 0);
|
|
assertResult(5, 1, 5);
|
|
assertResult(6, 2, 0);
|
|
assertResult(11, 2, 5);
|
|
assertResult(14, 3, 2);
|
|
|
|
document.body.removeChild(textarea);
|
|
});
|
|
|
|
test("toAsciiPrintable", function (assert) {
|
|
const accentedString = "Créme_Brûlée!";
|
|
const unicodeString = "談話";
|
|
|
|
assert.strictEqual(
|
|
toAsciiPrintable(accentedString, "discourse"),
|
|
"Creme_Brulee!",
|
|
"it replaces accented characters with the appropriate ASCII equivalent"
|
|
);
|
|
|
|
assert.strictEqual(
|
|
toAsciiPrintable(unicodeString, "discourse"),
|
|
"discourse",
|
|
"it uses the fallback string when unable to convert"
|
|
);
|
|
|
|
assert.strictEqual(
|
|
typeof toAsciiPrintable(unicodeString),
|
|
"undefined",
|
|
"it returns undefined when unable to convert and no fallback is provided"
|
|
);
|
|
});
|
|
|
|
test("slugify", function (assert) {
|
|
const asciiString = "--- 0__( Some-cool Discourse Site! )__0 --- ";
|
|
const accentedString = "Créme_Brûlée!";
|
|
const unicodeString = "談話";
|
|
|
|
assert.strictEqual(
|
|
slugify(asciiString),
|
|
"0-some-cool-discourse-site-0",
|
|
"it properly slugifies an ASCII string"
|
|
);
|
|
|
|
assert.strictEqual(
|
|
slugify(accentedString),
|
|
"crme-brle",
|
|
"it removes accented characters"
|
|
);
|
|
|
|
assert.strictEqual(
|
|
slugify(unicodeString),
|
|
"",
|
|
"it removes unicode characters"
|
|
);
|
|
});
|
|
|
|
test("fillMissingDates", function (assert) {
|
|
const startDate = "2017-11-12"; // YYYY-MM-DD
|
|
const endDate = "2017-12-12"; // YYYY-MM-DD
|
|
const data =
|
|
'[{"x":"2017-11-12","y":3},{"x":"2017-11-27","y":2},{"x":"2017-12-06","y":9},{"x":"2017-12-11","y":2}]';
|
|
|
|
assert.strictEqual(
|
|
fillMissingDates(JSON.parse(data), startDate, endDate).length,
|
|
31,
|
|
"it returns a JSON array with 31 dates"
|
|
);
|
|
});
|
|
|
|
test("inCodeBlock", function (assert) {
|
|
const texts = [
|
|
// CLOSED CODE BLOCKS:
|
|
"000\n\n 111\n\n000",
|
|
"000 `111` 000",
|
|
"000\n```\n111\n```\n000",
|
|
"000\n[code]111[/code]\n000",
|
|
// OPEN CODE BLOCKS:
|
|
"000\n\n 111",
|
|
"000 `111",
|
|
"000\n```\n111",
|
|
"000\n[code]111",
|
|
// COMPLEX TEST:
|
|
"000\n\n```\n111\n```\n\n000\n\n`111 111`\n\n000\n\n[code]\n111\n[/code]\n\n 111\n\t111\n\n000`111",
|
|
// INDENTED OPEN CODE BLOCKS:
|
|
// - Using tab
|
|
"000\n\t```111\n\t111\n\t111```\n000",
|
|
// - Using spaces
|
|
`000\n \`\`\`111\n 111\n 111\`\`\`\n000`,
|
|
];
|
|
|
|
texts.forEach((text) => {
|
|
for (let i = 0; i < text.length; ++i) {
|
|
if (text[i] === "0" || text[i] === "1") {
|
|
assert.strictEqual(inCodeBlock(text, i), text[i] === "1");
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
test("mergeSortedLists", function (assert) {
|
|
const comparator = (a, b) => b > a;
|
|
assert.deepEqual(
|
|
mergeSortedLists([], [1, 2, 3], comparator),
|
|
[1, 2, 3],
|
|
"it doesn't error when the first list is blank"
|
|
);
|
|
assert.deepEqual(
|
|
mergeSortedLists([3, 2, 1], [], comparator),
|
|
[3, 2, 1],
|
|
"it doesn't error when the second list is blank"
|
|
);
|
|
assert.deepEqual(
|
|
mergeSortedLists([], [], comparator),
|
|
[],
|
|
"it doesn't error when the both lists are blank"
|
|
);
|
|
assert.deepEqual(
|
|
mergeSortedLists([5, 4, 0, -1], [1], comparator),
|
|
[5, 4, 1, 0, -1],
|
|
"it correctly merges lists when one list has 1 item only"
|
|
);
|
|
assert.deepEqual(
|
|
mergeSortedLists([2], [1], comparator),
|
|
[2, 1],
|
|
"it correctly merges lists when both lists has 1 item each"
|
|
);
|
|
assert.deepEqual(
|
|
mergeSortedLists([1], [1], comparator),
|
|
[1, 1],
|
|
"it correctly merges lists when both lists has 1 item and their items are identical"
|
|
);
|
|
assert.deepEqual(
|
|
mergeSortedLists([5, 4, 3, 2, 1], [6, 2, 1], comparator),
|
|
[6, 5, 4, 3, 2, 2, 1, 1],
|
|
"it correctly merges lists that share common items"
|
|
);
|
|
});
|
|
});
|
|
|
|
module("Unit | Utilities | modKeysPressed", function (hooks) {
|
|
setupRenderingTest(hooks);
|
|
|
|
test("returns an array of modifier keys pressed during keyboard or mouse event", async function (assert) {
|
|
let i = 0;
|
|
|
|
this.handleClick = (event) => {
|
|
if (i === 0) {
|
|
assert.deepEqual(modKeysPressed(event), []);
|
|
} else if (i === 1) {
|
|
assert.deepEqual(modKeysPressed(event), ["alt"]);
|
|
} else if (i === 2) {
|
|
assert.deepEqual(modKeysPressed(event), ["shift"]);
|
|
} else if (i === 3) {
|
|
assert.deepEqual(modKeysPressed(event), ["meta"]);
|
|
} else if (i === 4) {
|
|
assert.deepEqual(modKeysPressed(event), ["ctrl"]);
|
|
} else if (i === 5) {
|
|
assert.deepEqual(modKeysPressed(event), [
|
|
"alt",
|
|
"shift",
|
|
"meta",
|
|
"ctrl",
|
|
]);
|
|
}
|
|
};
|
|
|
|
await render(hbs`<button id="btn" {{on "click" this.handleClick}} />`);
|
|
|
|
await click("#btn");
|
|
i++;
|
|
await click("#btn", { altKey: true });
|
|
i++;
|
|
await click("#btn", { shiftKey: true });
|
|
i++;
|
|
await click("#btn", { metaKey: true });
|
|
i++;
|
|
await click("#btn", { ctrlKey: true });
|
|
i++;
|
|
await click("#btn", {
|
|
altKey: true,
|
|
shiftKey: true,
|
|
metaKey: true,
|
|
ctrlKey: true,
|
|
});
|
|
});
|
|
});
|
|
|
|
module("Unit | Utilities | clipboard", function (hooks) {
|
|
setupTest(hooks);
|
|
|
|
hooks.beforeEach(function () {
|
|
this.mockClipboard = {
|
|
writeText: sinon.stub().resolves(true),
|
|
write: sinon.stub().resolves(true),
|
|
};
|
|
sinon.stub(window.navigator, "clipboard").get(() => this.mockClipboard);
|
|
});
|
|
|
|
async function asyncFunction() {
|
|
return new Blob(["some text to copy"], {
|
|
type: "text/plain",
|
|
});
|
|
}
|
|
|
|
test("clipboardCopyAsync - browser does not support window.ClipboardItem", async function (assert) {
|
|
// without this check the stubbing will fail on Firefox
|
|
if (window.ClipboardItem) {
|
|
sinon.stub(window, "ClipboardItem").value(null);
|
|
}
|
|
|
|
await clipboardCopyAsync(asyncFunction);
|
|
assert.strictEqual(
|
|
this.mockClipboard.writeText.calledWith("some text to copy"),
|
|
true,
|
|
"it writes to the clipboard using writeText instead of write"
|
|
);
|
|
});
|
|
|
|
chromeTest(
|
|
"clipboardCopyAsync - browser does support window.ClipboardItem",
|
|
async function (assert) {
|
|
await clipboardCopyAsync(asyncFunction);
|
|
assert.strictEqual(
|
|
this.mockClipboard.write.called,
|
|
true,
|
|
"it writes to the clipboard using write"
|
|
);
|
|
}
|
|
);
|
|
});
|