UNPKG

@samepage/testing

Version:

Utilities that help with testing SamePage-compatible extensions

537 lines 23.6 kB
"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