Consumers of this utility function (e.g. the chat sidebar) expect to be able to use the resultant URL without any further transformations. Previously, it was only returning the user_avatar path without any CDN consideration. This commit ensures the result will include the app CDN URL when enabled.
446 lines
12 KiB
JavaScript
446 lines
12 KiB
JavaScript
import { Promise } from "rsvp";
|
|
import {
|
|
avatarImg,
|
|
avatarUrl,
|
|
caretRowCol,
|
|
clipboardCopyAsync,
|
|
defaultHomepage,
|
|
emailValid,
|
|
escapeExpression,
|
|
extractDomainFromUrl,
|
|
fillMissingDates,
|
|
getRawSize,
|
|
inCodeBlock,
|
|
initializeDefaultHomepage,
|
|
mergeSortedLists,
|
|
modKeysPressed,
|
|
setCaretPosition,
|
|
setDefaultHomepage,
|
|
slugify,
|
|
toAsciiPrintable,
|
|
} from "discourse/lib/utilities";
|
|
import sinon from "sinon";
|
|
import { test } from "qunit";
|
|
import Handlebars from "handlebars";
|
|
import {
|
|
chromeTest,
|
|
discourseModule,
|
|
} 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";
|
|
|
|
discourseModule("Unit | Utilities", function () {
|
|
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("avatarUrl", function (assert) {
|
|
let rawSize = getRawSize;
|
|
assert.blank(avatarUrl("", "tiny"), "no template returns blank");
|
|
assert.strictEqual(
|
|
avatarUrl("/fake/template/{size}.png", "tiny"),
|
|
"/fake/template/" + rawSize(20) + ".png",
|
|
"simple avatar url"
|
|
);
|
|
assert.strictEqual(
|
|
avatarUrl("/fake/template/{size}.png", "large"),
|
|
"/fake/template/" + rawSize(45) + ".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/" + rawSize(45) + ".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='20' height='20' src='/path/to/avatar/40.png' class='avatar'>",
|
|
"it returns the avatar html"
|
|
);
|
|
|
|
assert.strictEqual(
|
|
avatarImg({
|
|
avatarTemplate,
|
|
size: "tiny",
|
|
title: "evilest trout",
|
|
}),
|
|
"<img loading='lazy' alt='' width='20' height='20' src='/path/to/avatar/40.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='20' height='20' src='/path/to/avatar/40.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);
|
|
initializeDefaultHomepage(this.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) {
|
|
this.siteSettings.top_menu = "top|latest|hot";
|
|
initializeDefaultHomepage(this.siteSettings);
|
|
assert.strictEqual(
|
|
defaultHomepage(),
|
|
"top",
|
|
"default homepage is the first item in the top_menu site setting"
|
|
);
|
|
});
|
|
|
|
test("setDefaultHomepage", function (assert) {
|
|
initializeDefaultHomepage(this.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"
|
|
);
|
|
});
|
|
|
|
discourseModule("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,
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
discourseModule("Unit | Utilities | clipboard", function (hooks) {
|
|
let mockClipboard;
|
|
hooks.beforeEach(function () {
|
|
mockClipboard = {
|
|
writeText: sinon.stub().resolves(true),
|
|
write: sinon.stub().resolves(true),
|
|
};
|
|
sinon.stub(window.navigator, "clipboard").get(() => mockClipboard);
|
|
});
|
|
|
|
function getPromiseFunction() {
|
|
return () =>
|
|
new Promise((resolve) => {
|
|
resolve(
|
|
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(getPromiseFunction());
|
|
assert.strictEqual(
|
|
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(getPromiseFunction());
|
|
assert.strictEqual(
|
|
mockClipboard.write.called,
|
|
true,
|
|
"it writes to the clipboard using write"
|
|
);
|
|
}
|
|
);
|
|
});
|