0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-02-22 23:06:08 -05:00

Adapt mock and add workspace test with websocket mock

This commit is contained in:
Belén Albeza 2024-05-03 14:52:58 +02:00 committed by AzazelN28
parent 30321e54f0
commit 3bae6e4661
19 changed files with 347 additions and 132 deletions

View file

@ -0,0 +1,58 @@
{
"~:features": {
"~#set": [
"layout/grid",
"styles/v2",
"fdata/pointer-map",
"fdata/objects-map",
"components/v2",
"fdata/shape-data-type"
]
},
"~:permissions": {
"~:type": "~:membership",
"~:is-owner": true,
"~:is-admin": true,
"~:can-edit": true,
"~:can-read": true,
"~:is-logged": true
},
"~:has-media-trimmed": false,
"~:comment-thread-seqn": 0,
"~:name": "New File 1",
"~:revn": 11,
"~:modified-at": "~m1713873823633",
"~:id": "~uc7ce0794-0992-8105-8004-38f280443849",
"~:is-shared": false,
"~:version": 46,
"~:project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b",
"~:created-at": "~m1713536343369",
"~:data": {
"~:pages": [
"~uc7ce0794-0992-8105-8004-38f28044384a"
],
"~:pages-index": {
"~uc7ce0794-0992-8105-8004-38f28044384a": {
"~#penpot/pointer": [
"~ude58c8f6-c5c2-8196-8004-3df9e2e52d88",
{
"~:created-at": "~m1713873823636"
}
]
}
},
"~:id": "~uc7ce0794-0992-8105-8004-38f280443849",
"~:options": {
"~:components-v2": true
},
"~:recent-colors": [
{
"~:color": "#0000ff",
"~:opacity": 1,
"~:id": null,
"~:file-id": null,
"~:image": null
}
]
}
}

View file

@ -0,0 +1,97 @@
{
"~:id": "~ude58c8f6-c5c2-8196-8004-3df9e2e52d88",
"~:file-id": "~uc7ce0794-0992-8105-8004-38f280443849",
"~:created-at": "~m1713873823631",
"~:content": {
"~:options": {},
"~:objects": {
"~u00000000-0000-0000-0000-000000000000": {
"~#shape": {
"~:y": 0,
"~:hide-fill-on-export": false,
"~:transform": {
"~#matrix": {
"~:a": 1,
"~:b": 0,
"~:c": 0,
"~:d": 1,
"~:e": 0,
"~:f": 0
}
},
"~:rotation": 0,
"~:name": "Root Frame",
"~:width": 0.01,
"~:type": "~:frame",
"~:points": [
{
"~#point": {
"~:x": 0,
"~:y": 0
}
},
{
"~#point": {
"~:x": 0.01,
"~:y": 0
}
},
{
"~#point": {
"~:x": 0.01,
"~:y": 0.01
}
},
{
"~#point": {
"~:x": 0,
"~:y": 0.01
}
}
],
"~:proportion-lock": false,
"~:transform-inverse": {
"~#matrix": {
"~:a": 1,
"~:b": 0,
"~:c": 0,
"~:d": 1,
"~:e": 0,
"~:f": 0
}
},
"~:id": "~u00000000-0000-0000-0000-000000000000",
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
"~:strokes": [],
"~:x": 0,
"~:proportion": 1,
"~:selrect": {
"~#rect": {
"~:x": 0,
"~:y": 0,
"~:width": 0.01,
"~:height": 0.01,
"~:x1": 0,
"~:y1": 0,
"~:x2": 0.01,
"~:y2": 0.01
}
},
"~:fills": [
{
"~:fill-color": "#FFFFFF",
"~:fill-opacity": 1
}
],
"~:flip-x": null,
"~:height": 0.01,
"~:flip-y": null,
"~:shapes": []
}
}
},
"~:id": "~uc7ce0794-0992-8105-8004-38f28044384a",
"~:name": "Page 1"
}
}

View file

@ -0,0 +1,3 @@
{
"c7ce0794-0992-8105-8004-38f280443849/c7ce0794-0992-8105-8004-38f28044384a/8c1035fa-01f0-8071-8004-3df966ff2c64/frame": "http://localhost:3449/assets/by-id/50d097ed-d321-4319-b00b-e82a9c9435ea"
}

View file

@ -0,0 +1,9 @@
[
{
"~:id": "~uc7ce0794-0992-8105-8004-38e630f29a9b",
"~:email": "foo@example.com",
"~:name": "Princesa Leia",
"~:fullname": "Princesa Leia",
"~:is-active": true
}
]

View file

@ -0,0 +1,8 @@
{
"~:id": "~uc7ce0794-0992-8105-8004-38e630f7920b",
"~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d",
"~:created-at": "~m1713533116382",
"~:modified-at": "~m1713873823633",
"~:is-default": true,
"~:name": "Drafts"
}

View file

@ -0,0 +1,23 @@
{
"~:features": {
"~#set": [
"layout/grid",
"styles/v2",
"fdata/pointer-map",
"fdata/objects-map",
"components/v2",
"fdata/shape-data-type"
]
},
"~:permissions": {
"~:type": "~:membership",
"~:is-owner": true,
"~:is-admin": true,
"~:can-edit": true
},
"~:name": "Default",
"~:modified-at": "~m1713533116375",
"~:id": "~uc7ce0794-0992-8105-8004-38e630f40f6d",
"~:created-at": "~m1713533116375",
"~:is-default": true
}

View file

@ -0,0 +1,7 @@
export const presenceFixture = {
"~:type": "~:presence",
"~:file-id": "~uc7ce0794-0992-8105-8004-38f280443849",
"~:session-id": "~u37730924-d520-80f1-8004-4ae6e5c3942d",
"~:profile-id": "~uc7ce0794-0992-8105-8004-38e630f29a9b",
"~:subs-id": "~uc7ce0794-0992-8105-8004-38f280443849",
};

View file

@ -0,0 +1,17 @@
export const interceptRPC = (page, path, jsonFilename) =>
page.route(`**/api/rpc/command/${path}`, (route) =>
route.fulfill({
status: 200,
contentType: "application/transit+json",
path: `playwright/fixtures/${jsonFilename}`,
}),
);
export const interceptRPCByRegex = (page, regex, jsonFilename) =>
page.route(regex, (route) =>
route.fulfill({
status: 200,
contentType: "application/transit+json",
path: `playwright/fixtures/${jsonFilename}`,
}),
);

View file

@ -1,8 +0,0 @@
export const interceptRPC = (page, path, jsonFilename) =>
page.route(`**/api/rpc/command/${path}`, (route) =>
route.fulfill({
status: 200,
contentType: "application/transit+json",
path: `playwright/fixtures/${jsonFilename}`,
})
);

View file

@ -1,81 +1,5 @@
export class MockWebSocket extends EventTarget {
static #mocks = new Map();
export class WebSocketManager {
static async init(page) {
await page.exposeFunction('MockWebSocket$$constructor', (url, protocols) => {
console.log('MockWebSocket$$constructor', MockWebSocket, url, protocols)
const webSocket = new MockWebSocket(page, url, protocols);
this.#mocks.set(url, webSocket);
});
await page.exposeFunction('MockWebSocket$$spyMessage', (url, data) => {
console.log('MockWebSocket$$spyMessage', url, data)
this.#mocks.get(url).dispatchEvent(new MessageEvent('message', { data }))
});
await page.exposeFunction('MockWebSocket$$spyClose', (url, code, reason) => {
console.log('MockWebSocket$$spyClose', url, code, reason)
this.#mocks.get(url).dispatchEvent(new CloseEvent('close', { code, reason }))
});
await page.addInitScript({ path: "playwright/scripts/MockWebSocket.js" });
}
static waitForURL(url) {
return new Promise((resolve) => {
let intervalID = setInterval(() => {
for (const [wsURL, ws] of this.#mocks) {
console.log(wsURL)
if (wsURL.includes(url)) {
clearInterval(intervalID);
return resolve(ws);
}
}
}, 30)
})
}
#page = null
#url
#protocols
// spies.
#spyClose = null
#spyMessage = null
constructor(page, url, protocols) {
super()
this.#page = page
this.#url = url
this.#protocols = protocols
}
mockOpen(options) {
return this.#page.evaluate((options) => {
WebSocket.getByURL(url).mockOpen(options)
}, options)
}
mockMessage(data) {
return this.#page.evaluate((data) => {
WebSocket.getByURL(url).mockMessage(data)
}, data)
}
mockClose() {
return this.#page.evaluate(() => {
WebSocket.getByURL(url).mockClose()
})
}
spyClose(fn) {
if (typeof fn !== 'function') {
throw new TypeError('Invalid callback')
}
this.#spyClose = fn
}
spyMessage(fn) {
if (typeof fn !== 'function') {
throw new TypeError('Invalid callback')
}
this.#spyMessage = fn
}
}

View file

@ -1,6 +1,5 @@
import { test, expect } from "@playwright/test";
import { interceptRPC } from "./helpers/MockRPC";
import { MockWebSocket } from "./helpers/MockWebSocket";
import { interceptRPC } from "./helpers/MockAPI";
const setupLoggedOutUser = async (page) => {
await interceptRPC(page, "get-profile", "get-profile-anonymous.json");
@ -30,17 +29,9 @@ const setupDashboardUser = async (page) => {
"get-profiles-for-file-comments",
"logged-in-user/get-profiles-for-file-comments-empty.json",
);
await interceptRPC(
page,
"get-builtin-templates",
"logged-in-user/get-builtin-templates-empty.json",
);
await interceptRPC(page, "get-builtin-templates", "logged-in-user/get-builtin-templates-empty.json");
};
test.beforeEach(async ({ page }) => {
await MockWebSocket.init(page);
})
test("Shows login page when going to index and user is logged out", async ({ page }) => {
await setupLoggedOutUser(page);
@ -62,8 +53,5 @@ test("User logs in by filling the login form", async ({ page }) => {
await page.getByRole("button", { name: "Login" }).click();
const ws = await MockWebSocket.waitForURL('ws://0.0.0.0:3500/ws/notifications');
console.log(ws)
await expect(page).toHaveURL(/dashboard/);
});

View file

@ -1,4 +1,3 @@
console.log("MockWebSocket mock loaded");
window.WebSocket = class MockWebSocket extends EventTarget {
static CONNECTING = 0;
static OPEN = 1;
@ -15,6 +14,19 @@ window.WebSocket = class MockWebSocket extends EventTarget {
return this.#mocks.get(url);
}
static waitForURL(url) {
return new Promise((resolve) => {
let intervalID = setInterval(() => {
for (const [wsURL, ws] of this.#mocks) {
if (wsURL.includes(url)) {
clearInterval(intervalID);
resolve(ws);
}
}
}, 30);
});
}
#url;
#protocols;
#protocol = "";
@ -32,10 +44,9 @@ window.WebSocket = class MockWebSocket extends EventTarget {
#spyClose = null;
constructor(url, protocols) {
console.log("🤖 New websocket at", url);
super();
console.log("MockWebSocket", url, protocols);
this.#url = url;
this.#protocols = protocols || [];
@ -84,13 +95,13 @@ window.WebSocket = class MockWebSocket extends EventTarget {
}
set onopen(callback) {
if (callback === null) {
this.removeEventListener("open", this.#onopen);
} else if (typeof callback === "function") {
if (this.#onopen) this.removeEventListener("open", this.#onopen);
this.removeEventListener("open", this.#onopen);
this.#onopen = null;
if (typeof callback === "function") {
this.addEventListener("open", callback);
this.#onopen = callback;
}
this.#onopen = callback;
}
get onopen() {
@ -98,13 +109,13 @@ window.WebSocket = class MockWebSocket extends EventTarget {
}
set onerror(callback) {
if (callback === null) {
this.removeEventListener("error", this.#onerror);
} else if (typeof callback === "function") {
if (this.#onerror) this.removeEventListener("error", this.#onerror);
this.removeEventListener("error", this.#onerror);
this.#onerror = null;
if (typeof callback === "function") {
this.addEventListener("error", callback);
this.#onerror = callback;
}
this.#onerror = callback;
}
get onerror() {
@ -112,13 +123,13 @@ window.WebSocket = class MockWebSocket extends EventTarget {
}
set onmessage(callback) {
if (callback === null) {
this.removeEventListener("message", this.#onmessage);
} else if (typeof callback === "function") {
if (this.#onmessage) this.removeEventListener("message", this.#onmessage);
this.removeEventListener("message", this.#onmessage);
this.#onmessage = null;
if (typeof callback === "function") {
this.addEventListener("message", callback);
this.#onmessage = callback;
}
this.#onmessage = callback;
}
get onmessage() {
@ -126,13 +137,13 @@ window.WebSocket = class MockWebSocket extends EventTarget {
}
set onclose(callback) {
if (callback === null) {
this.removeEventListener("close", this.#onclose);
} else if (typeof callback === "function") {
if (this.#onclose) this.removeEventListener("close", this.#onclose);
this.removeEventListener("close", this.#onclose);
this.#onclose = null;
if (typeof callback === "function") {
this.addEventListener("close", callback);
this.#onclose = callback;
}
this.#onclose = callback;
}
get onclose() {
@ -160,6 +171,7 @@ window.WebSocket = class MockWebSocket extends EventTarget {
}
mockOpen(options) {
console.log("🤖 open mock");
this.#protocol = options?.protocol || "";
this.#extensions = options?.extensions || "";
this.#readyState = MockWebSocket.OPEN;
@ -174,9 +186,12 @@ window.WebSocket = class MockWebSocket extends EventTarget {
}
mockMessage(data) {
console.log("🤯 mock message");
if (this.#readyState !== MockWebSocket.OPEN) {
console.log("socket is not connected");
throw new Error("MockWebSocket is not connected");
}
console.log("😰 dispatching `message`", { data });
this.dispatchEvent(new MessageEvent("message", { data }));
return this;
}
@ -188,16 +203,16 @@ window.WebSocket = class MockWebSocket extends EventTarget {
}
send(data) {
console.log(data);
if (this.#readyState === MockWebSocket.CONNECTING) {
throw new DOMException("InvalidStateError", "MockWebSocket is not connected");
}
console.log(`MockWebSocket send: ${data}`);
this.#spyMessage && this.#spyMessage(this.url, data);
if (this.#spyMessage) {
this.#spyMessage(this.url, data);
}
}
close(code, reason) {
console.log(code, reason);
if (code && !Number.isInteger(code) && code !== 1000 && (code < 3000 || code > 4999)) {
throw new DOMException("InvalidAccessError", "Invalid code");
}
@ -214,7 +229,8 @@ window.WebSocket = class MockWebSocket extends EventTarget {
}
this.#readyState = MockWebSocket.CLOSING;
console.log("MockWebSocket close");
this.#spyClose && this.#spyClose(this.url, code, reason);
if (this.#spyClose) {
this.#spyClose(this.url, code, reason);
}
}
}
};

View file

@ -0,0 +1,69 @@
import { test, expect } from "@playwright/test";
import { interceptRPC, interceptRPCByRegex } from "./helpers/MockAPI";
import { WebSocketManager } from "./helpers/MockWebSocket";
import { presenceFixture } from "./fixtures/workspace/ws-notifications";
const anyProjectId = "c7ce0794-0992-8105-8004-38e630f7920b";
const anyFileId = "c7ce0794-0992-8105-8004-38f280443849";
const anyPageId = "c7ce0794-0992-8105-8004-38f28044384a";
const setupWorkspaceUser = (page) => {
interceptRPC(page, "get-profile", "logged-in-user/get-profile-logged-in.json");
interceptRPC(page, "get-team-users?file-id=*", "logged-in-user/get-team-users-single-user.json");
interceptRPC(page, "get-comment-threads?file-id=*", "workspace/get-comment-threads-empty.json");
interceptRPC(page, "get-project?id=*", "workspace/get-project-default.json");
interceptRPC(page, "get-team?id=*", "workspace/get-team-default.json");
interceptRPCByRegex(page, /get\-file\?/, "workspace/get-file-blank.json");
interceptRPC(
page,
"get-file-object-thumbnails?file-id=*",
"workspace/get-file-object-thumbnails-blank.json",
);
interceptRPC(
page,
"get-profiles-for-file-comments?file-id=*",
"workspace/get-profile-for-file-comments.json",
);
interceptRPC(page, "get-font-variants?team-id=*", "workspace/get-font-variants-empty.json");
interceptRPC(page, "get-file-fragment?file-id=*", "workspace/get-file-fragment-blank.json");
interceptRPC(page, "get-file-libraries?file-id=*", "workspace/get-file-libraries-empty.json");
};
test.beforeEach(async ({ page }) => {
await WebSocketManager.init(page);
});
test("User loads worskpace with empty file", async ({ page }) => {
await setupWorkspaceUser(page);
await page.goto(`/#/workspace/${anyProjectId}/${anyFileId}?page-id=${anyPageId}`);
await expect(page.getByTestId("page-name")).toHaveText("Page 1");
});
test.only("User receives notifications updates in the workspace", async ({ page }) => {
await setupWorkspaceUser(page);
await page.goto(`/#/workspace/${anyProjectId}/${anyFileId}?page-id=${anyPageId}`);
await page.evaluate(async () => {
const ws = await WebSocket.waitForURL("ws://0.0.0.0:3500/ws/notifications");
ws.mockOpen();
});
await expect(page.getByTestId("page-name")).toHaveText("Page 1");
await page.evaluate(
async ({ presenceFixture }) => {
const ws = await WebSocket.waitForURL("ws://0.0.0.0:3500/ws/notifications");
ws.mockMessage(JSON.stringify(presenceFixture));
},
{ presenceFixture },
);
expect(page.getByTestId("active-users-list").getByAltText("Princesa Leia")).toHaveCount(2);
await page.evaluate(async () => {
const ws = await WebSocket.waitForURL("ws://0.0.0.0:3500/ws/notifications");
ws.mockClose();
});
});

View file

@ -162,6 +162,7 @@
(assoc :text-color "#000000")))
(update-presence [presence]
(js/console.log "🥰 WIIIIII" (clj->js presence))
(-> presence
(update session-id update-session presence)
(d/without-nils)))]

View file

@ -56,7 +56,7 @@
:class (stl/css :active-users-opened)
:on-click on-close
:on-blur on-close}
[:ul {:class (stl/css :active-users-list)}
[:ul {:class (stl/css :active-users-list) :data-testid "active-users-list"}
(for [session sessions]
[:& session-widget
{:color (:color session)
@ -66,7 +66,7 @@
[:button {:class (stl/css-case :active-users true)
:on-click on-open}
[:ul {:class (stl/css :active-users-list)}
[:ul {:class (stl/css :active-users-list) :data-testid "active-users-list"}
(when (> num-sessions 2)
[:span {:class (stl/css :users-num)} (dm/str "+" (- num-sessions 2))])

View file

@ -144,7 +144,7 @@
:auto-focus true
:default-value (:name page "")}]]
[:*
[:span {:class (stl/css :page-name)}
[:span {:class (stl/css :page-name) :data-testid "page-name"}
(:name page)]
[:div {:class (stl/css :page-actions)}
(when (and deletable? (not workspace-read-only?))