[CORL-1013] Count Reset (#2960)

* feat: added reset option for count.js

* fix: adjust reset beheviour

* fix: switched to bundlesize2

* fix: added flag to enable github checks

Co-authored-by: Vinh <vinh@vinh.tech>
This commit is contained in:
Wyatt Johnson
2020-05-13 16:47:45 +00:00
committed by GitHub
parent 79e0da2c2f
commit f73597d7d1
8 changed files with 356 additions and 85 deletions
+9 -7
View File
@@ -1,8 +1,8 @@
# job_environment will setup the environment for any job being executed.
job_environment: &job_environment
NODE_ENV: test
WEBPACK_MAX_CORES: 4
NODE_OPTIONS: --max-old-space-size=8192
NODE_ENV: "test"
WEBPACK_MAX_CORES: "4"
NODE_OPTIONS: "--max-old-space-size=8192"
# job_defaults applies all the defaults for each job.
job_defaults: &job_defaults
@@ -49,7 +49,8 @@ jobs:
- ~/.npm
- persist_to_workspace:
root: .
paths: node_modules
paths:
- node_modules
# lint will perform file linting.
lint:
@@ -78,7 +79,7 @@ jobs:
<<: *job_defaults
environment:
<<: *job_environment
CI: true
CI: "true"
JEST_JUNIT_OUTPUT: "reports/junit/js-test-results.xml"
steps:
- checkout
@@ -115,14 +116,15 @@ jobs:
no_output_timeout: 30m
- run:
name: Verify Bundle Size
command: npx bundlesize
command: npx bundlesize2 --enable-github-checks
- save_cache:
key: v1-build-cache-{{ .Branch }}-{{ .Revision }}
paths:
- ./dist
- persist_to_workspace:
root: .
paths: dist
paths:
- dist
# docker_tests will test that the docker build process completes.
docker_tests:
+4
View File
@@ -420,6 +420,10 @@
{
"path": "./dist/static/assets/js/embed.js",
"maxSize": "15 kB"
},
{
"path": "./dist/static/assets/js/count.js",
"maxSize": "2 kB"
}
],
"graphql-schema-linter": {
+48 -8
View File
@@ -10,49 +10,89 @@ import injectJSONPCallback from "./injectJSONPCallback";
interface CountQueryArgs {
id?: string;
url?: string;
notext?: boolean;
notext: boolean;
}
/** createCountQueryRef creates a unique reference from the query args */
function createCountQueryRef(args: CountQueryArgs) {
return btoa(`${JSON.stringify(!!args.notext)};${args.id || args.url}`);
return btoa(`${args.notext ? "true" : "false"};${args.id || args.url}`);
}
interface DetectAndInjectArgs {
reset?: boolean;
}
/** Detects count elements and use jsonp to inject the counts. */
function detectAndInject() {
function detectAndInject(opts: DetectAndInjectArgs = {}) {
const ORIGIN = getCurrentScriptOrigin(ORIGIN_FALLBACK_ID);
const STORY_URL = resolveStoryURL();
/** A map of references pointing to the count query arguments */
const queryMap: Record<string, CountQueryArgs> = {};
// Find all the selected elements and fill the queryMap.
const elements = document.querySelectorAll(COUNT_SELECTOR);
Array.prototype.forEach.call(elements, (element: HTMLElement) => {
let url = element.dataset.coralUrl;
const id = element.dataset.coralId;
const notext = element.dataset.coralNotext === "true";
// If there is no URL or ID on the element, add one based on the story url
// that we detected.
let url = element.dataset.coralUrl;
if (!url && !id) {
url = STORY_URL;
element.dataset.coralUrl = STORY_URL;
}
// Construct the args for generating the ref.
const args = { id, url, notext };
const ref = createCountQueryRef(args);
// Get or create a ref.
let ref = element.dataset.coralRef;
if (!ref) {
ref = createCountQueryRef(args);
element.dataset.coralRef = ref;
} else {
// The element already had a ref attached to it, which means it's already
// been processed. If we aren't resetting, we should skip this.
if (!opts.reset) {
return;
}
}
// Add it to the managed set if we haven't already.
if (!(ref in queryMap)) {
queryMap[ref] = args;
}
element.dataset.coralRef = ref;
});
// Call server using JSONP.
Object.keys(queryMap).forEach((ref) => {
const { url, id, notext } = queryMap[ref];
const args = { url, id, notext: notext ? "true" : "false", ref };
// Compile the arguments used to generate the
const args: Record<string, string | number | undefined> = {
url,
id,
notext: notext ? "true" : "false",
ref,
};
// Special handling for when the count is reset.
if (opts.reset) {
// Add the date as an argument to cache bust.
args.d = Date.now().toString();
}
// Add the script element with the specified options to the page.
jsonp(`${ORIGIN}/api/story/count.js`, "CoralCount.setCount", args);
});
}
export function main() {
injectJSONPCallback();
// Inject the JSONP callback with the detection script to be used as the
// CoralCount.getCount callback.
injectJSONPCallback(detectAndInject);
detectAndInject();
}
+4 -1
View File
@@ -1,7 +1,9 @@
import { COUNT_SELECTOR } from "coral-framework/constants";
type GetCountFunction = (opts?: { reset?: boolean }) => void;
/** Injects a global CoralCount callback into the window object to be used in JSONP */
function injectJSONPCallback() {
function injectJSONPCallback(getCount: GetCountFunction) {
(window as any).CoralCount = {
setCount: (data: { ref: string; html: string }) => {
// Find all the elements with ref.
@@ -12,6 +14,7 @@ function injectJSONPCallback() {
element.innerHTML = data.html;
});
},
getCount,
};
}
@@ -24,13 +24,205 @@ exports[`Calls JSONP 1`] = `
data-coral-url="http://localhost:8080/"
/>
<script
src="http://localhost:8080/api/story/count.js?callback=CoralCount.setCount&url=http%3A%2F%2Flocalhost%3A8080%2Fstory.html&notext=false&ref=ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwL3N0b3J5Lmh0bWw%3D"
src="http://localhost:8080/api/story/count.js?callback=CoralCount.setCount&notext=false&ref=ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwL3N0b3J5Lmh0bWw%3D&url=http%3A%2F%2Flocalhost%3A8080%2Fstory.html"
/>
<script
src="http://localhost:8080/api/story/count.js?callback=CoralCount.setCount&id=1234-5678-91021&notext=false&ref=ZmFsc2U7MTIzNC01Njc4LTkxMDIx"
/>
<script
src="http://localhost:8080/api/story/count.js?callback=CoralCount.setCount&url=http%3A%2F%2Flocalhost%3A8080%2F&notext=false&ref=ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwLw%3D%3D"
src="http://localhost:8080/api/story/count.js?callback=CoralCount.setCount&notext=false&ref=ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwLw%3D%3D&url=http%3A%2F%2Flocalhost%3A8080%2F"
/>
</body>
`;
exports[`Calls JSONP again 1`] = `
<body>
<span
class="coral-count"
data-coral-ref="ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwL3N0b3J5Lmh0bWw="
data-coral-url="http://localhost:8080/story.html"
/>
<span
class="coral-count"
data-coral-id="1234-5678-91021"
data-coral-ref="ZmFsc2U7MTIzNC01Njc4LTkxMDIx"
/>
<span
class="coral-count"
data-coral-ref="ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwLw=="
data-coral-url="http://localhost:8080/"
data-notext="true"
/>
<span
class="coral-count"
data-coral-ref="ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwLw=="
data-coral-url="http://localhost:8080/"
/>
<script
src="http://localhost:8080/api/story/count.js?callback=CoralCount.setCount&notext=false&ref=ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwL3N0b3J5Lmh0bWw%3D&url=http%3A%2F%2Flocalhost%3A8080%2Fstory.html"
/>
<script
src="http://localhost:8080/api/story/count.js?callback=CoralCount.setCount&id=1234-5678-91021&notext=false&ref=ZmFsc2U7MTIzNC01Njc4LTkxMDIx"
/>
<script
src="http://localhost:8080/api/story/count.js?callback=CoralCount.setCount&notext=false&ref=ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwLw%3D%3D&url=http%3A%2F%2Flocalhost%3A8080%2F"
/>
</body>
`;
exports[`Calls JSONP again 2`] = `
<body>
<span
class="coral-count"
data-coral-ref="ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwL3N0b3J5Lmh0bWw="
data-coral-url="http://localhost:8080/story.html"
/>
<span
class="coral-count"
data-coral-id="1234-5678-91021"
data-coral-ref="ZmFsc2U7MTIzNC01Njc4LTkxMDIx"
/>
<span
class="coral-count"
data-coral-ref="ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwLw=="
data-coral-url="http://localhost:8080/"
data-notext="true"
/>
<span
class="coral-count"
data-coral-ref="ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwLw=="
data-coral-url="http://localhost:8080/"
/>
<script
src="http://localhost:8080/api/story/count.js?callback=CoralCount.setCount&notext=false&ref=ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwL3N0b3J5Lmh0bWw%3D&url=http%3A%2F%2Flocalhost%3A8080%2Fstory.html"
/>
<script
src="http://localhost:8080/api/story/count.js?callback=CoralCount.setCount&id=1234-5678-91021&notext=false&ref=ZmFsc2U7MTIzNC01Njc4LTkxMDIx"
/>
<script
src="http://localhost:8080/api/story/count.js?callback=CoralCount.setCount&notext=false&ref=ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwLw%3D%3D&url=http%3A%2F%2Flocalhost%3A8080%2F"
/>
</body>
`;
exports[`Calls JSONP again 3`] = `
<body>
<span
class="coral-count"
data-coral-ref="ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwL3N0b3J5Lmh0bWw="
data-coral-url="http://localhost:8080/story.html"
/>
<span
class="coral-count"
data-coral-id="1234-5678-91021"
data-coral-ref="ZmFsc2U7MTIzNC01Njc4LTkxMDIx"
/>
<span
class="coral-count"
data-coral-ref="ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwLw=="
data-coral-url="http://localhost:8080/"
data-notext="true"
/>
<span
class="coral-count"
data-coral-ref="ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwLw=="
data-coral-url="http://localhost:8080/"
/>
<script
src="http://localhost:8080/api/story/count.js?callback=CoralCount.setCount&notext=false&ref=ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwL3N0b3J5Lmh0bWw%3D&url=http%3A%2F%2Flocalhost%3A8080%2Fstory.html"
/>
<script
src="http://localhost:8080/api/story/count.js?callback=CoralCount.setCount&id=1234-5678-91021&notext=false&ref=ZmFsc2U7MTIzNC01Njc4LTkxMDIx"
/>
<script
src="http://localhost:8080/api/story/count.js?callback=CoralCount.setCount&notext=false&ref=ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwLw%3D%3D&url=http%3A%2F%2Flocalhost%3A8080%2F"
/>
<span
class="coral-count"
data-coral-id="another-coral-id"
data-coral-ref="ZmFsc2U7YW5vdGhlci1jb3JhbC1pZA=="
/>
<script
src="http://localhost:8080/api/story/count.js?callback=CoralCount.setCount&id=another-coral-id&notext=false&ref=ZmFsc2U7YW5vdGhlci1jb3JhbC1pZA%3D%3D"
/>
</body>
`;
exports[`Calls JSONP again with reset 1`] = `
<body>
<span
class="coral-count"
data-coral-ref="ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwL3N0b3J5Lmh0bWw="
data-coral-url="http://localhost:8080/story.html"
/>
<span
class="coral-count"
data-coral-id="1234-5678-91021"
data-coral-ref="ZmFsc2U7MTIzNC01Njc4LTkxMDIx"
/>
<span
class="coral-count"
data-coral-ref="ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwLw=="
data-coral-url="http://localhost:8080/"
data-notext="true"
/>
<span
class="coral-count"
data-coral-ref="ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwLw=="
data-coral-url="http://localhost:8080/"
/>
<script
src="http://localhost:8080/api/story/count.js?callback=CoralCount.setCount&notext=false&ref=ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwL3N0b3J5Lmh0bWw%3D&url=http%3A%2F%2Flocalhost%3A8080%2Fstory.html"
/>
<script
src="http://localhost:8080/api/story/count.js?callback=CoralCount.setCount&id=1234-5678-91021&notext=false&ref=ZmFsc2U7MTIzNC01Njc4LTkxMDIx"
/>
<script
src="http://localhost:8080/api/story/count.js?callback=CoralCount.setCount&notext=false&ref=ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwLw%3D%3D&url=http%3A%2F%2Flocalhost%3A8080%2F"
/>
</body>
`;
exports[`Calls JSONP again with reset 2`] = `
<body>
<span
class="coral-count"
data-coral-ref="ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwL3N0b3J5Lmh0bWw="
data-coral-url="http://localhost:8080/story.html"
/>
<span
class="coral-count"
data-coral-id="1234-5678-91021"
data-coral-ref="ZmFsc2U7MTIzNC01Njc4LTkxMDIx"
/>
<span
class="coral-count"
data-coral-ref="ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwLw=="
data-coral-url="http://localhost:8080/"
data-notext="true"
/>
<span
class="coral-count"
data-coral-ref="ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwLw=="
data-coral-url="http://localhost:8080/"
/>
<script
src="http://localhost:8080/api/story/count.js?callback=CoralCount.setCount&notext=false&ref=ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwL3N0b3J5Lmh0bWw%3D&url=http%3A%2F%2Flocalhost%3A8080%2Fstory.html"
/>
<script
src="http://localhost:8080/api/story/count.js?callback=CoralCount.setCount&id=1234-5678-91021&notext=false&ref=ZmFsc2U7MTIzNC01Njc4LTkxMDIx"
/>
<script
src="http://localhost:8080/api/story/count.js?callback=CoralCount.setCount&notext=false&ref=ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwLw%3D%3D&url=http%3A%2F%2Flocalhost%3A8080%2F"
/>
<script
src="http://localhost:8080/api/story/count.js?callback=CoralCount.setCount&d=1589310827300&notext=false&ref=ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwL3N0b3J5Lmh0bWw%3D&url=http%3A%2F%2Flocalhost%3A8080%2Fstory.html"
/>
<script
src="http://localhost:8080/api/story/count.js?callback=CoralCount.setCount&d=1589310827300&id=1234-5678-91021&notext=false&ref=ZmFsc2U7MTIzNC01Njc4LTkxMDIx"
/>
<script
src="http://localhost:8080/api/story/count.js?callback=CoralCount.setCount&d=1589310827300&notext=false&ref=ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwLw%3D%3D&url=http%3A%2F%2Flocalhost%3A8080%2F"
/>
</body>
`;
@@ -83,13 +275,13 @@ exports[`Inject counts 1`] = `
</span>
</span>
<script
src="http://localhost:8080/api/story/count.js?callback=CoralCount.setCount&url=http%3A%2F%2Flocalhost%3A8080%2Fstory.html&notext=false&ref=ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwL3N0b3J5Lmh0bWw%3D"
src="http://localhost:8080/api/story/count.js?callback=CoralCount.setCount&notext=false&ref=ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwL3N0b3J5Lmh0bWw%3D&url=http%3A%2F%2Flocalhost%3A8080%2Fstory.html"
/>
<script
src="http://localhost:8080/api/story/count.js?callback=CoralCount.setCount&id=1234-5678-91021&notext=false&ref=ZmFsc2U7MTIzNC01Njc4LTkxMDIx"
/>
<script
src="http://localhost:8080/api/story/count.js?callback=CoralCount.setCount&url=http%3A%2F%2Flocalhost%3A8080%2F&notext=false&ref=ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwLw%3D%3D"
src="http://localhost:8080/api/story/count.js?callback=CoralCount.setCount&notext=false&ref=ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwLw%3D%3D&url=http%3A%2F%2Flocalhost%3A8080%2F"
/>
</body>
`;
+32 -4
View File
@@ -1,4 +1,8 @@
import timekeeper from "timekeeper";
beforeAll(async () => {
timekeeper.freeze(new Date(1589310827300));
const script = document.createElement("script");
script.src = "http://localhost:8080/assets/js/count.js";
Object.defineProperty(window.document, "currentScript", {
@@ -11,6 +15,17 @@ beforeAll(async () => {
document.head.appendChild(link);
});
afterAll(() => {
timekeeper.reset();
});
function attachTag(attrs: object) {
const element = document.createElement("span");
element.className = "coral-count";
Object.assign(element.dataset, attrs);
document.body.appendChild(element);
}
beforeEach(async () => {
document.body.innerHTML = "";
const tags = [
@@ -27,11 +42,9 @@ beforeEach(async () => {
];
tags.forEach((attrs) => {
const element = document.createElement("span");
element.className = "coral-count";
Object.assign(element.dataset, attrs);
document.body.appendChild(element);
attachTag(attrs);
});
(await import("../")).main();
});
@@ -43,6 +56,21 @@ it("Calls JSONP", async () => {
expect(document.body).toMatchSnapshot();
});
it("Calls JSONP again", async () => {
expect(document.body).toMatchSnapshot();
(window as any).CoralCount.getCount();
expect(document.body).toMatchSnapshot();
attachTag({ coralId: "another-coral-id" });
(window as any).CoralCount.getCount();
expect(document.body).toMatchSnapshot();
});
it("Calls JSONP again with reset", async () => {
expect(document.body).toMatchSnapshot();
(window as any).CoralCount.getCount({ reset: true });
expect(document.body).toMatchSnapshot();
});
it("Inject counts", async () => {
(window as any).CoralCount.setCount({
ref: "ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwLw==",
+19 -13
View File
@@ -8,22 +8,28 @@
function jsonp(
endpoint: string,
callback: string,
args: Record<string, string | number | null | undefined>
args: Record<string, string | number | undefined>
) {
// Create the script element.
const script = document.createElement("script");
script.src = `${endpoint}?callback=${callback}`;
Object.keys(args).forEach((key) => {
let val = "";
if (args[key] === undefined) {
return;
}
if (typeof args[key] === "string") {
val = args[key] as string;
} else {
val = JSON.stringify(args[key]);
}
script.src += `&${key}=${encodeURIComponent(val)}`;
});
// For each of the arguments, add to the source string.
Object.keys(args)
// Because ordering of the keys isn't well defined, we sort the keys to
// ensure consistent ordering.
.sort()
.forEach((key) => {
const val = args[key];
if (val === undefined) {
return;
}
// Append the new parameter to the source.
script.src += `&${key}=${encodeURIComponent(val)}`;
});
// Attach the script to the body.
document.body.appendChild(script);
}
+44 -48
View File
@@ -1,5 +1,5 @@
import createDOMPurify from "dompurify";
import { JSDOM } from "jsdom";
// import createDOMPurify from "dompurify";
// import { JSDOM } from "jsdom";
import { AppOptions } from "coral-server/app";
import { calculateTotalPublishedCommentCount } from "coral-server/models/comment";
@@ -15,54 +15,50 @@ export type CountOptions = Pick<AppOptions, "mongo" | "tenantCache" | "i18n">;
/**
* countHandler returns translated comment counts using JSONP.
*/
export const countHandler = ({ mongo, i18n }: CountOptions): RequestHandler => {
const window = new JSDOM("").window;
const DOMPurify = createDOMPurify(window as any);
export const countHandler = ({
mongo,
i18n,
}: CountOptions): RequestHandler => async (req, res, next) => {
try {
// Tenant is guaranteed at this point.
const coral = req.coral!;
const tenant = coral.tenant!;
return async (req, res, next) => {
try {
// Tenant is guaranteed at this point.
const coral = req.coral!;
const tenant = coral.tenant!;
const story = await find(mongo, tenant, {
id: req.query.id,
url: req.query.url,
});
const story = await find(mongo, tenant, {
id: req.query.id,
url: req.query.url,
});
const count = story
? calculateTotalPublishedCommentCount(story.commentCounts.status)
: 0;
const count = story
? calculateTotalPublishedCommentCount(story.commentCounts.status)
: 0;
let html = "";
if (req.query.notext === "true") {
// We only need the count without the text.
html = `<span class="${NUMBER_CLASSNAME}">${count}</span>`;
} else {
// Use translated string.
const bundle = i18n.getBundle(tenant.locale);
html = translate(
bundle,
`<span class="${NUMBER_CLASSNAME}">${count}</span> <span class="${TEXT_CLASSNAME}">Comments</span>`,
"comment-count",
{
number: count,
numberClass: NUMBER_CLASSNAME,
textClass: TEXT_CLASSNAME,
}
);
// Strip dangerous html from translation.
html = DOMPurify.sanitize(html);
}
// Respond using jsonp.
res.jsonp({
// Reference from the client that we'll just send back as it is.
ref: req.query.ref,
html,
});
} catch (err) {
return next(err);
let html = "";
if (req.query.notext === "true") {
// We only need the count without the text.
html = `<span class="${NUMBER_CLASSNAME}">${count}</span>`;
} else {
// Use translated string.
const bundle = i18n.getBundle(tenant.locale);
html = translate(
bundle,
`<span class="${NUMBER_CLASSNAME}">${count}</span> <span class="${TEXT_CLASSNAME}">Comments</span>`,
"comment-count",
{
number: count,
numberClass: NUMBER_CLASSNAME,
textClass: TEXT_CLASSNAME,
}
);
}
};
// Respond using jsonp.
res.jsonp({
// Reference from the client that we'll just send back as it is.
ref: req.query.ref,
html,
});
} catch (err) {
return next(err);
}
};