@samepage/testing
Version:
Utilities that help with testing SamePage-compatible extensions
537 lines • 23.6 kB
JavaScript
"use strict";
var _a;
Object.defineProperty(exports, "__esModule", { value: true });
exports.responseMessageSchema = void 0;
const tslib_1 = require("tslib");
const registerAppEventListener_1 = require("../internal/registerAppEventListener");
const setupSamePageClient_1 = tslib_1.__importDefault(require("../protocols/setupSamePageClient"));
const sharePageWithNotebook_1 = tslib_1.__importDefault(require("../protocols/sharePageWithNotebook"));
const notebookQuerying_1 = tslib_1.__importDefault(require("../protocols/notebookQuerying"));
const types_1 = require("../internal/types");
const zod_1 = require("zod");
const ws_1 = tslib_1.__importDefault(require("ws"));
const node_fetch_1 = tslib_1.__importDefault(require("node-fetch"));
const inviteNotebookToPage_1 = tslib_1.__importDefault(require("../utils/inviteNotebookToPage"));
const toAtJson_1 = tslib_1.__importDefault(require("./toAtJson"));
const jsdom_1 = require("jsdom");
const apiClient_1 = tslib_1.__importDefault(require("../internal/apiClient"));
const automerge_1 = tslib_1.__importDefault(require("automerge"));
const base64ToBinary_1 = tslib_1.__importDefault(require("../internal/base64ToBinary"));
const notificationActions_1 = require("../internal/notificationActions");
const fromAtJson_1 = tslib_1.__importDefault(require("./fromAtJson"));
const localAutomergeDb_1 = require("../utils/localAutomergeDb");
const binaryToBase64_1 = tslib_1.__importDefault(require("../internal/binaryToBase64"));
const uuid_1 = require("uuid");
const changeAutomergeDoc_1 = tslib_1.__importDefault(require("../utils/changeAutomergeDoc"));
const unwrapSchema_1 = tslib_1.__importDefault(require("../utils/unwrapSchema"));
const debugger_1 = tslib_1.__importDefault(require("../utils/debugger"));
const SUPPORTED_TAGS = ["SPAN", "DIV", "A", "LI"];
const TAG_SET = new Set(SUPPORTED_TAGS);
const processMessageSchema = zod_1.z.discriminatedUnion("type", [
zod_1.z.object({
type: zod_1.z.literal("accept"),
notebookPageId: zod_1.z.string(),
notificationUuid: zod_1.z.string(),
data: types_1.zJsonData,
}),
zod_1.z.object({
type: zod_1.z.literal("accept-request"),
data: types_1.zJsonData,
notificationUuid: zod_1.z.string(),
}),
zod_1.z.object({
type: zod_1.z.literal("setCurrentNotebookPageId"),
notebookPageId: zod_1.z.string(),
}),
zod_1.z.object({
type: zod_1.z.literal("setAppClientState"),
notebookPageId: zod_1.z.string(),
data: zod_1.z.string(),
}),
zod_1.z.object({ type: zod_1.z.literal("share") }),
zod_1.z.object({
type: zod_1.z.literal("invite"),
notebookUuid: zod_1.z.string(),
}),
zod_1.z.object({ type: zod_1.z.literal("unload") }),
zod_1.z.object({ type: zod_1.z.literal("read"), notebookPageId: zod_1.z.string() }),
zod_1.z.object({
type: zod_1.z.literal("insert"),
notebookPageId: zod_1.z.string(),
content: zod_1.z.string(),
index: zod_1.z.number().or(zod_1.z.string()),
path: zod_1.z.string(),
delay: zod_1.z.literal(true).optional(),
}),
zod_1.z.object({
type: zod_1.z.literal("delete"),
notebookPageId: zod_1.z.string(),
count: zod_1.z.number(),
index: zod_1.z.number(),
path: zod_1.z.string(),
}),
zod_1.z.object({
type: zod_1.z.literal("disconnect"),
}),
zod_1.z.object({
type: zod_1.z.literal("connect"),
}),
zod_1.z.object({
type: zod_1.z.literal("break"),
}),
zod_1.z.object({
type: zod_1.z.literal("fix"),
}),
zod_1.z.object({
type: zod_1.z.literal("breakCalculate"),
}),
zod_1.z.object({
type: zod_1.z.literal("fixCalculate"),
}),
zod_1.z.object({
type: zod_1.z.literal("awaitLog"),
id: zod_1.z.string(),
}),
zod_1.z.object({
type: zod_1.z.literal("query"),
request: zod_1.z.string(),
}),
zod_1.z.object({
type: zod_1.z.literal("getSharedPage"),
notebookPageId: zod_1.z.string(),
}),
zod_1.z.object({
type: zod_1.z.literal("refresh"),
notebookPageId: zod_1.z.string(),
data: types_1.zInitialSchema,
}),
zod_1.z.object({
type: zod_1.z.literal("waitForNotification"),
}),
zod_1.z.object({
type: zod_1.z.literal("resume"),
notebookPageId: zod_1.z.string(),
update: zod_1.z.string(),
}),
zod_1.z.object({
type: zod_1.z.literal("route"),
routes: zod_1.z
.object({
key: zod_1.z.string(),
value: zod_1.z.string(),
response: types_1.zJsonData,
})
.array(),
}),
zod_1.z.object({
type: zod_1.z.literal("request"),
request: zod_1.z.any(),
target: zod_1.z.string(),
}),
]);
exports.responseMessageSchema = zod_1.z.discriminatedUnion("type", [
zod_1.z.object({
type: zod_1.z.literal("error"),
data: zod_1.z.string(),
}),
zod_1.z.object({
type: zod_1.z.literal("ready"),
uuid: zod_1.z.string(),
}),
zod_1.z.object({
type: zod_1.z.literal("response"),
uuid: zod_1.z.string(),
data: zod_1.z.record(zod_1.z.unknown()).optional(),
}),
]);
const forkedIndex = process.argv.indexOf("--forked");
const isForked = forkedIndex >= 0 && typeof process.send !== "undefined";
const createTestSamePageClient = async ({ workspace, initOptions, onMessage, }) => {
// @ts-ignore
global.WebSocket = ws_1.default;
// @ts-ignore
global.fetch = node_fetch_1.default;
const log = (0, debugger_1.default)(`test-client-${workspace}`);
const awaitLog = (id, target = 1) => new Promise((resolve) => {
let count = 0;
const offAppEvent = (0, registerAppEventListener_1.onAppEvent)("log", (e) => {
log(`while waiting for ${id} logged ${e.id}: ${JSON.stringify(e)}`);
if (e.id === id) {
count++;
if (count === target) {
offAppEvent();
resolve({ content: e.content, intent: e.intent });
}
}
});
});
let currentNotebookPageId = "";
const appClientState = {};
const commands = {};
const defaultApplyState = async (id, data) => {
appClientState[id] = new jsdom_1.JSDOM((0, fromAtJson_1.default)(data));
};
let applyState = defaultApplyState;
const defaultCalculateState = async (id) => {
const dom = appClientState[id];
if (dom) {
const atJson = (0, toAtJson_1.default)(dom.window.document.body);
return atJson;
}
else {
return { content: "", annotations: [] };
}
};
let calculateState = defaultCalculateState;
const settings = {
uuid: "",
token: "",
};
const initializingPromise = new Promise((resolve) => {
const offAppEvent = (0, registerAppEventListener_1.onAppEvent)("prompt-account-info", (e) => {
offAppEvent();
e.respond(initOptions).then(resolve);
});
});
log("setting up client");
const { unload, addNotebookRequestListener, sendNotebookRequest } = (0, setupSamePageClient_1.default)({
getSetting: (s) => settings[s],
setSetting: (s, v) => (settings[s] = v),
addCommand: ({ label, callback }) => (commands[label] = callback),
removeCommand: ({ label }) => delete commands[label],
workspace,
app: "samepage",
});
log("setting up page sync protocol");
const { unload: unloadSharePage, refreshContent, isShared, } = (0, sharePageWithNotebook_1.default)({
getCurrentNotebookPageId: async () => currentNotebookPageId,
ensurePageByTitle: async (title) => {
if (appClientState[title.content]) {
return { notebookPageId: title.content, preExisting: true };
}
appClientState[title.content] = new jsdom_1.JSDOM();
return title.content;
},
deletePage: async (notebookPageId) => delete appClientState[notebookPageId],
encodeState: async (notebookPageId) => ({
$body: await calculateState(notebookPageId),
}),
decodeState: async (id, data) => applyState(id, data.$body),
});
log("setting up page deprecated query protocol");
const { unload: unloadNotebookQuerying, query } = (0, notebookQuerying_1.default)({
onQuery: async (notebookPageId) => {
const dom = appClientState[notebookPageId];
return dom
? { ...(0, toAtJson_1.default)(dom.window.document.body) }
: { content: "", annotations: [] };
},
onQueryResponse: async (response) => {
if (response.data.content) {
const [notebookUuid, notebookPageId] = response.request.split(":");
appClientState[`${notebookUuid}:${notebookPageId}`] = new jsdom_1.JSDOM((0, fromAtJson_1.default)(response.data));
}
},
});
log("Check if account is finished initializing");
await initializingPromise.catch(() => onMessage({
type: "error",
data: `Error: 3 arguments required for --forked (workspace, email, password)\nFound: ${process.argv}`,
}));
log("Wait for success message");
await awaitLog("samepage-success");
onMessage({ type: "ready", uuid: settings.uuid });
const updatesToSend = {};
log("Ready for messages");
return {
send: async (m) => {
try {
const message = processMessageSchema
.and(zod_1.z.object({ uuid: zod_1.z.string().uuid() }))
.parse(m);
const sendResponse = (data) => onMessage({ type: "response", uuid: message.uuid, data });
if (message.type === "setCurrentNotebookPageId") {
currentNotebookPageId = message.notebookPageId;
sendResponse();
}
else if (message.type === "setAppClientState") {
appClientState[message.notebookPageId] = new jsdom_1.JSDOM(message.data);
const shared = await isShared(message.notebookPageId);
if (shared) {
await refreshContent({
notebookPageId: message.notebookPageId,
}).then(() => sendResponse({ success: true }));
}
else {
sendResponse({ success: false });
}
}
else if (message.type === "share") {
commands["Share Page on SamePage"]();
await awaitLog("init-page-success").then(() => sendResponse());
}
else if (message.type === "accept") {
(0, notificationActions_1.callNotificationAction)({
operation: "SHARE_PAGE",
label: "accept",
data: {
...message.data,
$title: { content: message.notebookPageId, annotations: [] },
},
messageUuid: message.notificationUuid,
}).then(() => sendResponse({ success: true }));
}
else if (message.type === "accept-request") {
(0, notificationActions_1.callNotificationAction)({
operation: "REQUEST_DATA",
label: "accept",
data: message.data,
messageUuid: message.notificationUuid,
}).then(() => sendResponse({ success: true }));
}
else if (message.type === "read") {
const dom = appClientState[message.notebookPageId];
sendResponse({
html: dom ? dom.window.document.body.innerHTML : "",
});
}
else if (message.type === "unload") {
unloadNotebookQuerying();
unloadSharePage();
unload();
sendResponse();
if (isForked)
process.disconnect();
}
else if (message.type === "invite") {
await Promise.all([
awaitLog("share-page-success"),
(0, inviteNotebookToPage_1.default)({
notebookPageId: currentNotebookPageId,
notebookUuid: message.notebookUuid,
}),
]).then(() => sendResponse());
}
else if (message.type === "waitForNotification") {
await new Promise((resolve) => {
const offAppEvent = (0, registerAppEventListener_1.onAppEvent)("notification", (e) => {
offAppEvent();
resolve(e.notification);
});
}).then((data) => {
sendResponse(data);
});
}
else if (message.type === "insert") {
const dom = appClientState[message.notebookPageId];
const el = dom.window.document.querySelector(message.path);
if (!el) {
onMessage({
type: "error",
data: `Failed to insert: cannot find ${message.path}\nDom: ${dom.window.document.body.innerHTML}`,
});
}
else if (TAG_SET.has(message.content)) {
const newEl = dom.window.document.createElement(message.content.toLowerCase());
const index = Number(message.index);
if (index >= el.children.length) {
el.appendChild(newEl);
}
else {
el.insertBefore(newEl, el.children[index]);
}
}
else if (typeof message.index === "string") {
el.setAttribute(message.index, message.content);
}
else {
const oldContent = el.textContent || "";
el.textContent = `${oldContent.slice(0, message.index)}${message.content}${oldContent.slice(message.index)}`;
}
if (el) {
if (message.delay) {
const newDoc = await calculateState(message.notebookPageId);
return (0, localAutomergeDb_1.load)(message.notebookPageId).then((oldDoc) => {
const doc = automerge_1.default.change(oldDoc, "refresh", async (oldDoc) => {
(0, changeAutomergeDoc_1.default)(oldDoc, newDoc);
});
(0, localAutomergeDb_1.set)(message.notebookPageId, doc);
const updateUuid = (0, uuid_1.v4)();
updatesToSend[updateUuid] = {
method: "update-shared-page",
changes: automerge_1.default.getChanges(oldDoc, doc).map(binaryToBase64_1.default),
notebookPageId: message.notebookPageId,
state: (0, binaryToBase64_1.default)(automerge_1.default.save(doc)),
};
sendResponse({ success: true, delayed: updateUuid });
});
}
await refreshContent({
notebookPageId: message.notebookPageId,
}).then(() => sendResponse({ success: true }));
}
}
else if (message.type === "delete") {
const dom = appClientState[message.notebookPageId];
const el = dom.window.document.body.querySelector(message.path);
if (el) {
if (message.count === 0) {
el.remove();
}
else {
const oldContent = el.textContent || "";
el.textContent = `${oldContent.slice(0, message.index)}${oldContent.slice(message.index + message.count)}`;
}
await refreshContent({
notebookPageId: message.notebookPageId,
}).then(() => sendResponse({ success: false }));
}
else {
onMessage({
type: "error",
data: `Failed to delete: cannot find ${message.path}`,
});
}
}
else if (message.type === "resume") {
const body = updatesToSend[message.update];
if (body) {
await (0, apiClient_1.default)(body);
sendResponse({ success: true });
}
else {
sendResponse({ success: false });
}
}
else if (message.type === "disconnect") {
const awaitDisconnect = awaitLog("samepage-disconnect");
commands["Disconnect from SamePage Network"]();
awaitDisconnect.then(() => sendResponse());
}
else if (message.type === "connect") {
commands["Connect to SamePage Network"]();
awaitLog("samepage-success").then(() => sendResponse());
}
else if (message.type === "break") {
applyState = () => Promise.reject(new Error("Something went wrong..."));
sendResponse();
}
else if (message.type === "fix") {
applyState = defaultApplyState;
sendResponse();
}
else if (message.type === "breakCalculate") {
// @ts-expect-error
calculateState = async () => ({
content: "Invalid",
annotations: [{ type: "invalid", start: 0, end: 7 }],
});
sendResponse();
}
else if (message.type === "fixCalculate") {
calculateState = defaultCalculateState;
sendResponse();
}
else if (message.type === "awaitLog") {
await awaitLog(message.id).then(sendResponse);
}
else if (message.type === "getSharedPage") {
await (0, apiClient_1.default)({
method: "get-shared-page",
notebookPageId: message.notebookPageId,
})
.then(({ state }) => {
const data = automerge_1.default.load((0, base64ToBinary_1.default)(state));
sendResponse((0, unwrapSchema_1.default)(data));
})
.catch((e) => {
return {
type: "response",
uuid: message.uuid,
data: e.message,
};
});
}
else if (message.type === "refresh") {
await applyState(message.notebookPageId, message.data);
await refreshContent({ notebookPageId: message.notebookPageId });
sendResponse({ success: true });
}
else if (message.type === "query") {
const data = await query(message.request);
sendResponse(data);
}
else if (message.type === "route") {
addNotebookRequestListener(({ request }) => {
log(`Was requested ${JSON.stringify(request)}`);
const route = message.routes.find((rte) => request[rte.key] === rte.value);
return route === null || route === void 0 ? void 0 : route.response;
});
sendResponse();
}
else if (message.type === "request") {
const id = (0, uuid_1.v4)();
const response = await sendNotebookRequest({
label: `Test ${id}`,
request: message.request,
target: message.target,
});
sendResponse({ response, id });
}
else {
onMessage({
type: "error",
data: `Unknown message type: ${message["type"]}`,
});
}
}
catch (e) {
onMessage({
type: "error",
data: e.stack || e.message,
});
}
},
};
};
if (isForked) {
if (process.argv.length > forkedIndex + 2)
createTestSamePageClient({
workspace: process.argv[forkedIndex + 1],
initOptions: {
email: process.argv[forkedIndex + 2],
password: process.argv[forkedIndex + 3],
create: process.argv.indexOf("--create") > -1,
},
onMessage: process.send.bind(process),
})
.then((client) => {
process.on("message", client.send);
process.on("unhandledRejection", (e) => {
var _a;
(_a = process.send) === null || _a === void 0 ? void 0 : _a.call(process, {
type: "error",
data: `UNHANDLED REJECTION: ${e === null || e === void 0 ? void 0 : e.stack}`,
});
});
process.on("uncaughtException", (e) => {
var _a;
(_a = process.send) === null || _a === void 0 ? void 0 : _a.call(process, { type: "error", data: `UNCAUGHT EXCEPTION: ${e}` });
});
})
.catch((e) => {
var _a;
(_a = process.send) === null || _a === void 0 ? void 0 : _a.call(process, {
type: "error",
data: e.stack || e.message,
});
});
else {
(_a = process.send) === null || _a === void 0 ? void 0 : _a.call(process, {
type: "error",
data: `Error: 3 arguments required for --forked (workspace, notebook id, token)\nFound: ${process.argv}`,
});
}
}
exports.default = createTestSamePageClient;
//# sourceMappingURL=createTestSamePageClient.js.map