From 30321e54f0928ae1aecd76d87e44cd6f147f3b04 Mon Sep 17 00:00:00 2001 From: AzazelN28 Date: Thu, 25 Apr 2024 14:14:46 +0200 Subject: [PATCH 1/4] :paperclip: Add WebSocket mock --- .gitignore | 4 + .../get-builtin-templates-empty.json | 1 + frontend/playwright/helpers/MockRPC.js | 8 + frontend/playwright/helpers/MockWebSocket.js | 81 +++++++ frontend/playwright/helpers/index.js | 14 -- frontend/playwright/login.spec.js | 69 ++++++ frontend/playwright/scripts/MockWebSocket.js | 220 ++++++++++++++++++ package-lock.json | 91 ++++++++ package.json | 4 + playwright.config.ts | 77 ++++++ yarn.lock | 44 +++- 11 files changed, 589 insertions(+), 24 deletions(-) create mode 100644 frontend/playwright/data/logged-in-user/get-builtin-templates-empty.json create mode 100644 frontend/playwright/helpers/MockRPC.js create mode 100644 frontend/playwright/helpers/MockWebSocket.js delete mode 100644 frontend/playwright/helpers/index.js create mode 100644 frontend/playwright/login.spec.js create mode 100644 frontend/playwright/scripts/MockWebSocket.js create mode 100644 package-lock.json create mode 100644 playwright.config.ts diff --git a/.gitignore b/.gitignore index 0e271d125..9b3cdd4b0 100644 --- a/.gitignore +++ b/.gitignore @@ -68,3 +68,7 @@ clj-profiler/ node_modules frontend/.storybook/preview-body.html +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/frontend/playwright/data/logged-in-user/get-builtin-templates-empty.json b/frontend/playwright/data/logged-in-user/get-builtin-templates-empty.json new file mode 100644 index 000000000..0637a088a --- /dev/null +++ b/frontend/playwright/data/logged-in-user/get-builtin-templates-empty.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/frontend/playwright/helpers/MockRPC.js b/frontend/playwright/helpers/MockRPC.js new file mode 100644 index 000000000..b08b9678d --- /dev/null +++ b/frontend/playwright/helpers/MockRPC.js @@ -0,0 +1,8 @@ +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}`, + }) + ); diff --git a/frontend/playwright/helpers/MockWebSocket.js b/frontend/playwright/helpers/MockWebSocket.js new file mode 100644 index 000000000..53d7612b7 --- /dev/null +++ b/frontend/playwright/helpers/MockWebSocket.js @@ -0,0 +1,81 @@ +export class MockWebSocket extends EventTarget { + static #mocks = new Map(); + + 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 + } +} diff --git a/frontend/playwright/helpers/index.js b/frontend/playwright/helpers/index.js deleted file mode 100644 index ac8108f81..000000000 --- a/frontend/playwright/helpers/index.js +++ /dev/null @@ -1,14 +0,0 @@ -export const interceptRPC = async (page, path, jsonFilename, options = {}) => { - const interceptConfig = { - status: 200, - ...options, - }; - - await page.route(`**/api/rpc/command/${path}`, async (route) => { - await route.fulfill({ - ...interceptConfig, - contentType: "application/transit+json", - path: `playwright/data/${jsonFilename}`, - }); - }); -}; diff --git a/frontend/playwright/login.spec.js b/frontend/playwright/login.spec.js new file mode 100644 index 000000000..18279b95a --- /dev/null +++ b/frontend/playwright/login.spec.js @@ -0,0 +1,69 @@ +import { test, expect } from "@playwright/test"; +import { interceptRPC } from "./helpers/MockRPC"; +import { MockWebSocket } from "./helpers/MockWebSocket"; + +const setupLoggedOutUser = async (page) => { + await interceptRPC(page, "get-profile", "get-profile-anonymous.json"); + await interceptRPC(page, "login-with-password", "logged-in-user/login-with-password-success.json"); +}; + +// TODO: maybe Playwright's fixtures are the right way to do this? +const setupDashboardUser = async (page) => { + await interceptRPC(page, "get-profile", "logged-in-user/get-profile-logged-in.json"); + await interceptRPC(page, "get-teams", "logged-in-user/get-teams-default.json"); + await interceptRPC(page, "get-font-variants?team-id=*", "logged-in-user/get-font-variants-empty.json"); + await interceptRPC(page, "get-projects?team-id=*", "logged-in-user/get-projects-default.json"); + await interceptRPC(page, "get-team-members?team-id=*", "logged-in-user/get-team-members-your-penpot.json"); + await interceptRPC(page, "get-team-users?team-id=*", "logged-in-user/get-team-users-single-user.json"); + await interceptRPC( + page, + "get-unread-comment-threads?team-id=*", + "logged-in-user/get-team-users-single-user.json", + ); + await interceptRPC( + page, + "get-team-recent-files?team-id=*", + "logged-in-user/get-team-recent-files-empty.json", + ); + await interceptRPC( + 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", + ); +}; + +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); + + await page.goto("/"); + + await expect(page).toHaveURL(/auth\/login$/); + await expect(page.getByText("Log into my account")).toBeVisible(); +}); + +test("User logs in by filling the login form", async ({ page }) => { + await setupLoggedOutUser(page); + + await page.goto("/#/auth/login"); + + await setupDashboardUser(page); + + await page.getByLabel("Email").fill("foo@example.com"); + await page.getByLabel("Password").fill("loremipsum"); + + 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/); +}); diff --git a/frontend/playwright/scripts/MockWebSocket.js b/frontend/playwright/scripts/MockWebSocket.js new file mode 100644 index 000000000..457955449 --- /dev/null +++ b/frontend/playwright/scripts/MockWebSocket.js @@ -0,0 +1,220 @@ +console.log("MockWebSocket mock loaded"); +window.WebSocket = class MockWebSocket extends EventTarget { + static CONNECTING = 0; + static OPEN = 1; + static CLOSING = 2; + static CLOSED = 3; + + static #mocks = new Map(); + + static getAll() { + return this.#mocks.values(); + } + + static getByURL(url) { + return this.#mocks.get(url); + } + + #url; + #protocols; + #protocol = ""; + #binaryType = "blob"; + #bufferedAmount = 0; + #extensions = ""; + #readyState = MockWebSocket.CONNECTING; + + #onopen = null; + #onerror = null; + #onmessage = null; + #onclose = null; + + #spyMessage = null; + #spyClose = null; + + constructor(url, protocols) { + super(); + + console.log("MockWebSocket", url, protocols); + + this.#url = url; + this.#protocols = protocols || []; + + MockWebSocket.#mocks.set(this.#url, this); + + if (typeof window["MockWebSocket$$constructor"] === "function") { + MockWebSocket$$constructor(this.#url, this.#protocols); + } + if (typeof window["MockWebSocket$$spyMessage"] === "function") { + this.#spyMessage = MockWebSocket$$spyMessage; + } + if (typeof window["MockWebSocket$$spyClose"] === "function") { + this.#spyClose = MockWebSocket$$spyClose; + } + } + + set binaryType(binaryType) { + if (!["blob", "arraybuffer"].includes(binaryType)) { + return; + } + this.#binaryType = binaryType; + } + + get binaryType() { + return this.#binaryType; + } + + get bufferedAmount() { + return this.#bufferedAmount; + } + + get extensions() { + return this.#extensions; + } + + get readyState() { + return this.#readyState; + } + + get protocol() { + return this.#protocol; + } + + get url() { + return this.#url; + } + + 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.addEventListener("open", callback); + } + this.#onopen = callback; + } + + get onopen() { + return this.#onopen; + } + + 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.addEventListener("error", callback); + } + this.#onerror = callback; + } + + get onerror() { + return this.#onerror; + } + + 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.addEventListener("message", callback); + } + this.#onmessage = callback; + } + + get onmessage() { + return this.#onmessage; + } + + 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.addEventListener("close", callback); + } + this.#onclose = callback; + } + + get onclose() { + return this.#onclose; + } + + get mockProtocols() { + return this.#protocols; + } + + spyClose(callback) { + if (typeof callback !== "function") { + throw new TypeError("Invalid callback"); + } + this.#spyClose = callback; + return this; + } + + spyMessage(callback) { + if (typeof callback !== "function") { + throw new TypeError("Invalid callback"); + } + this.#spyMessage = callback; + return this; + } + + mockOpen(options) { + this.#protocol = options?.protocol || ""; + this.#extensions = options?.extensions || ""; + this.#readyState = MockWebSocket.OPEN; + this.dispatchEvent(new Event("open")); + return this; + } + + mockError(error) { + this.#readyState = MockWebSocket.CLOSED; + this.dispatchEvent(new ErrorEvent("error", { error })); + return this; + } + + mockMessage(data) { + if (this.#readyState !== MockWebSocket.OPEN) { + throw new Error("MockWebSocket is not connected"); + } + this.dispatchEvent(new MessageEvent("message", { data })); + return this; + } + + mockClose(code, reason) { + this.#readyState = MockWebSocket.CLOSED; + this.dispatchEvent(new CloseEvent("close", { code: code || 1000, reason: reason || "" })); + return this; + } + + 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); + } + + close(code, reason) { + console.log(code, reason); + if (code && !Number.isInteger(code) && code !== 1000 && (code < 3000 || code > 4999)) { + throw new DOMException("InvalidAccessError", "Invalid code"); + } + + if (reason && typeof reason === "string") { + const reasonBytes = new TextEncoder().encode(reason); + if (reasonBytes.length > 123) { + throw new DOMException("SyntaxError", "Reason is too long"); + } + } + + if ([MockWebSocket.CLOSED, MockWebSocket.CLOSING].includes(this.#readyState)) { + return; + } + + this.#readyState = MockWebSocket.CLOSING; + console.log("MockWebSocket close"); + this.#spyClose && this.#spyClose(this.url, code, reason); + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..8c0dc2cbc --- /dev/null +++ b/package-lock.json @@ -0,0 +1,91 @@ +{ + "name": "penpot", + "version": "1.20.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "penpot", + "version": "1.20.0", + "license": "MPL-2.0", + "devDependencies": { + "@playwright/test": "^1.43.1", + "@types/node": "^20.12.7" + } + }, + "node_modules/@playwright/test": { + "version": "1.43.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.43.1.tgz", + "integrity": "sha512-HgtQzFgNEEo4TE22K/X7sYTYNqEMMTZmFS8kTq6m8hXj+m1D8TgwgIbumHddJa9h4yl4GkKb8/bgAl2+g7eDgA==", + "dev": true, + "dependencies": { + "playwright": "1.43.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@types/node": { + "version": "20.12.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", + "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.43.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.43.1.tgz", + "integrity": "sha512-V7SoH0ai2kNt1Md9E3Gwas5B9m8KR2GVvwZnAI6Pg0m3sh7UvgiYhRrhsziCmqMJNouPckiOhk8T+9bSAK0VIA==", + "dev": true, + "dependencies": { + "playwright-core": "1.43.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.43.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.43.1.tgz", + "integrity": "sha512-EI36Mto2Vrx6VF7rm708qSnesVQKbxEWvPrfA1IPY6HgczBplDx7ENtx+K2n4kJ41sLLkuGfmb0ZLSSXlDhqPg==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + } + } +} diff --git a/package.json b/package.json index 94f482998..d26aae7be 100644 --- a/package.json +++ b/package.json @@ -18,5 +18,9 @@ "lint:clj:backend": "clj-kondo --parallel=true --lint backend/src", "lint:clj:exporter": "clj-kondo --parallel=true --lint exporter/src", "lint:clj": "yarn run lint:clj:common && yarn run lint:clj:frontend && yarn run lint:clj:backend && yarn run lint:clj:exporter" + }, + "devDependencies": { + "@playwright/test": "^1.43.1", + "@types/node": "^20.12.7" } } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 000000000..301801ee1 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,77 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://127.0.0.1:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/yarn.lock b/yarn.lock index 9a4b9536a..b3d605679 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1,12 +1,36 @@ -# This file is generated by running "yarn install" inside your project. -# Manual changes might be lost - proceed with caution! +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 -__metadata: - version: 8 - cacheKey: 10c0 -"penpot@workspace:.": - version: 0.0.0-use.local - resolution: "penpot@workspace:." - languageName: unknown - linkType: soft +"@playwright/test@^1.43.1": + version "1.43.1" + resolved "https://registry.npmjs.org/@playwright/test/-/test-1.43.1.tgz" + integrity sha512-HgtQzFgNEEo4TE22K/X7sYTYNqEMMTZmFS8kTq6m8hXj+m1D8TgwgIbumHddJa9h4yl4GkKb8/bgAl2+g7eDgA== + dependencies: + playwright "1.43.1" + +"@types/node@^20.12.7": + version "20.12.7" + resolved "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz" + integrity sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg== + dependencies: + undici-types "~5.26.4" + +playwright-core@1.43.1: + version "1.43.1" + resolved "https://registry.npmjs.org/playwright-core/-/playwright-core-1.43.1.tgz" + integrity sha512-EI36Mto2Vrx6VF7rm708qSnesVQKbxEWvPrfA1IPY6HgczBplDx7ENtx+K2n4kJ41sLLkuGfmb0ZLSSXlDhqPg== + +playwright@1.43.1: + version "1.43.1" + resolved "https://registry.npmjs.org/playwright/-/playwright-1.43.1.tgz" + integrity sha512-V7SoH0ai2kNt1Md9E3Gwas5B9m8KR2GVvwZnAI6Pg0m3sh7UvgiYhRrhsziCmqMJNouPckiOhk8T+9bSAK0VIA== + dependencies: + playwright-core "1.43.1" + optionalDependencies: + fsevents "2.3.2" + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== From 3bae6e46618e7927a92fba02de6ac61713fde020 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Fri, 3 May 2024 14:52:58 +0200 Subject: [PATCH 2/4] :sparkles: Adapt mock and add workspace test with websocket mock --- .../workspace/get-comment-threads-empty.json | 1 + .../fixtures/workspace/get-file-blank.json | 58 +++++++++++ .../workspace/get-file-fragment-blank.json | 97 +++++++++++++++++++ .../workspace/get-file-libraries-empty.json | 1 + .../get-file-object-thumbnails-blank.json | 3 + .../workspace/get-font-variants-empty.json | 1 + .../get-profile-for-file-comments.json | 9 ++ .../workspace/get-project-default.json | 8 ++ .../fixtures/workspace/get-team-default.json | 23 +++++ .../fixtures/workspace/ws-notifications.js | 7 ++ frontend/playwright/helpers/MockAPI.js | 17 ++++ frontend/playwright/helpers/MockRPC.js | 8 -- frontend/playwright/helpers/MockWebSocket.js | 78 +-------------- frontend/playwright/login.spec.js | 16 +-- frontend/playwright/scripts/MockWebSocket.js | 76 +++++++++------ frontend/playwright/workspace.spec.js | 69 +++++++++++++ .../main/data/workspace/notifications.cljs | 1 + .../src/app/main/ui/workspace/presence.cljs | 4 +- .../main/ui/workspace/sidebar/sitemap.cljs | 2 +- 19 files changed, 347 insertions(+), 132 deletions(-) create mode 100644 frontend/playwright/fixtures/workspace/get-comment-threads-empty.json create mode 100644 frontend/playwright/fixtures/workspace/get-file-blank.json create mode 100644 frontend/playwright/fixtures/workspace/get-file-fragment-blank.json create mode 100644 frontend/playwright/fixtures/workspace/get-file-libraries-empty.json create mode 100644 frontend/playwright/fixtures/workspace/get-file-object-thumbnails-blank.json create mode 100644 frontend/playwright/fixtures/workspace/get-font-variants-empty.json create mode 100644 frontend/playwright/fixtures/workspace/get-profile-for-file-comments.json create mode 100644 frontend/playwright/fixtures/workspace/get-project-default.json create mode 100644 frontend/playwright/fixtures/workspace/get-team-default.json create mode 100644 frontend/playwright/fixtures/workspace/ws-notifications.js create mode 100644 frontend/playwright/helpers/MockAPI.js delete mode 100644 frontend/playwright/helpers/MockRPC.js create mode 100644 frontend/playwright/workspace.spec.js diff --git a/frontend/playwright/fixtures/workspace/get-comment-threads-empty.json b/frontend/playwright/fixtures/workspace/get-comment-threads-empty.json new file mode 100644 index 000000000..0637a088a --- /dev/null +++ b/frontend/playwright/fixtures/workspace/get-comment-threads-empty.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/frontend/playwright/fixtures/workspace/get-file-blank.json b/frontend/playwright/fixtures/workspace/get-file-blank.json new file mode 100644 index 000000000..9e05e3b50 --- /dev/null +++ b/frontend/playwright/fixtures/workspace/get-file-blank.json @@ -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 + } + ] + } +} \ No newline at end of file diff --git a/frontend/playwright/fixtures/workspace/get-file-fragment-blank.json b/frontend/playwright/fixtures/workspace/get-file-fragment-blank.json new file mode 100644 index 000000000..fe357c500 --- /dev/null +++ b/frontend/playwright/fixtures/workspace/get-file-fragment-blank.json @@ -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" + } +} \ No newline at end of file diff --git a/frontend/playwright/fixtures/workspace/get-file-libraries-empty.json b/frontend/playwright/fixtures/workspace/get-file-libraries-empty.json new file mode 100644 index 000000000..0637a088a --- /dev/null +++ b/frontend/playwright/fixtures/workspace/get-file-libraries-empty.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/frontend/playwright/fixtures/workspace/get-file-object-thumbnails-blank.json b/frontend/playwright/fixtures/workspace/get-file-object-thumbnails-blank.json new file mode 100644 index 000000000..8f55ece27 --- /dev/null +++ b/frontend/playwright/fixtures/workspace/get-file-object-thumbnails-blank.json @@ -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" +} \ No newline at end of file diff --git a/frontend/playwright/fixtures/workspace/get-font-variants-empty.json b/frontend/playwright/fixtures/workspace/get-font-variants-empty.json new file mode 100644 index 000000000..0637a088a --- /dev/null +++ b/frontend/playwright/fixtures/workspace/get-font-variants-empty.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/frontend/playwright/fixtures/workspace/get-profile-for-file-comments.json b/frontend/playwright/fixtures/workspace/get-profile-for-file-comments.json new file mode 100644 index 000000000..f11319ecf --- /dev/null +++ b/frontend/playwright/fixtures/workspace/get-profile-for-file-comments.json @@ -0,0 +1,9 @@ +[ + { + "~:id": "~uc7ce0794-0992-8105-8004-38e630f29a9b", + "~:email": "foo@example.com", + "~:name": "Princesa Leia", + "~:fullname": "Princesa Leia", + "~:is-active": true + } +] \ No newline at end of file diff --git a/frontend/playwright/fixtures/workspace/get-project-default.json b/frontend/playwright/fixtures/workspace/get-project-default.json new file mode 100644 index 000000000..d953da8fd --- /dev/null +++ b/frontend/playwright/fixtures/workspace/get-project-default.json @@ -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" +} \ No newline at end of file diff --git a/frontend/playwright/fixtures/workspace/get-team-default.json b/frontend/playwright/fixtures/workspace/get-team-default.json new file mode 100644 index 000000000..e31dcf90c --- /dev/null +++ b/frontend/playwright/fixtures/workspace/get-team-default.json @@ -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 +} diff --git a/frontend/playwright/fixtures/workspace/ws-notifications.js b/frontend/playwright/fixtures/workspace/ws-notifications.js new file mode 100644 index 000000000..4ab58d147 --- /dev/null +++ b/frontend/playwright/fixtures/workspace/ws-notifications.js @@ -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", +}; diff --git a/frontend/playwright/helpers/MockAPI.js b/frontend/playwright/helpers/MockAPI.js new file mode 100644 index 000000000..8a5e970c4 --- /dev/null +++ b/frontend/playwright/helpers/MockAPI.js @@ -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}`, + }), + ); diff --git a/frontend/playwright/helpers/MockRPC.js b/frontend/playwright/helpers/MockRPC.js deleted file mode 100644 index b08b9678d..000000000 --- a/frontend/playwright/helpers/MockRPC.js +++ /dev/null @@ -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}`, - }) - ); diff --git a/frontend/playwright/helpers/MockWebSocket.js b/frontend/playwright/helpers/MockWebSocket.js index 53d7612b7..18134ff78 100644 --- a/frontend/playwright/helpers/MockWebSocket.js +++ b/frontend/playwright/helpers/MockWebSocket.js @@ -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 - } } diff --git a/frontend/playwright/login.spec.js b/frontend/playwright/login.spec.js index 18279b95a..cade62385 100644 --- a/frontend/playwright/login.spec.js +++ b/frontend/playwright/login.spec.js @@ -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/); }); diff --git a/frontend/playwright/scripts/MockWebSocket.js b/frontend/playwright/scripts/MockWebSocket.js index 457955449..a7449827e 100644 --- a/frontend/playwright/scripts/MockWebSocket.js +++ b/frontend/playwright/scripts/MockWebSocket.js @@ -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); + } } -} +}; diff --git a/frontend/playwright/workspace.spec.js b/frontend/playwright/workspace.spec.js new file mode 100644 index 000000000..ff0bc5fee --- /dev/null +++ b/frontend/playwright/workspace.spec.js @@ -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(); + }); +}); diff --git a/frontend/src/app/main/data/workspace/notifications.cljs b/frontend/src/app/main/data/workspace/notifications.cljs index 4bb3a9772..b721c2485 100644 --- a/frontend/src/app/main/data/workspace/notifications.cljs +++ b/frontend/src/app/main/data/workspace/notifications.cljs @@ -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)))] diff --git a/frontend/src/app/main/ui/workspace/presence.cljs b/frontend/src/app/main/ui/workspace/presence.cljs index cdf6c6e23..6a071fbda 100644 --- a/frontend/src/app/main/ui/workspace/presence.cljs +++ b/frontend/src/app/main/ui/workspace/presence.cljs @@ -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))]) diff --git a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs index 975615048..39064f7c7 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs @@ -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?)) From 572c6f02e25adea959807d4ea8a82300b4d1c590 Mon Sep 17 00:00:00 2001 From: AzazelN28 Date: Tue, 7 May 2024 11:18:35 +0200 Subject: [PATCH 3/4] :recycle: Refactor MockWebSocket --- frontend/playwright/helpers/MockWebSocket.js | 69 +++++++++++++++++++- frontend/playwright/scripts/MockWebSocket.js | 23 +++---- frontend/playwright/workspace.spec.js | 21 ++++-- 3 files changed, 91 insertions(+), 22 deletions(-) diff --git a/frontend/playwright/helpers/MockWebSocket.js b/frontend/playwright/helpers/MockWebSocket.js index 18134ff78..1a6c8eb81 100644 --- a/frontend/playwright/helpers/MockWebSocket.js +++ b/frontend/playwright/helpers/MockWebSocket.js @@ -1,5 +1,72 @@ -export class WebSocketManager { +export class MockWebSocket extends EventTarget { + static #mocks = new Map(); + 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) => { + const intervalID = setInterval(() => { + for (const [wsURL, ws] of this.#mocks) { + console.log('waitForURL', wsURL); + if (wsURL.includes(url)) { + clearInterval(intervalID); + return resolve(ws); + } + } + }, 30); + }); + } + + #page = null; + #url; + #protocols; + + constructor(page, url, protocols) { + super(); + this.#page = page; + this.#url = url; + this.#protocols = protocols; + } + + mockOpen(options) { + return this.#page.evaluate(({ url, options }) => { + if (typeof WebSocket.getByURL !== 'function') { + throw new Error('WebSocket.getByURL is not a function. Did you forget to call MockWebSocket.init(page)?') + } + WebSocket.getByURL(url).mockOpen(options); + }, { url: this.#url, options }); + } + + mockMessage(data) { + return this.#page.evaluate(({ url, data }) => { + if (typeof WebSocket.getByURL !== 'function') { + throw new Error('WebSocket.getByURL is not a function. Did you forget to call MockWebSocket.init(page)?') + } + WebSocket.getByURL(url).mockMessage(data); + }, { url: this.#url, data }); + } + + mockClose() { + return this.#page.evaluate(({ url }) => { + if (typeof WebSocket.getByURL !== 'function') { + throw new Error('WebSocket.getByURL is not a function. Did you forget to call MockWebSocket.init(page)?') + } + WebSocket.getByURL(url).mockClose(); + }, { url: this.#url }); + } } diff --git a/frontend/playwright/scripts/MockWebSocket.js b/frontend/playwright/scripts/MockWebSocket.js index a7449827e..60723eb29 100644 --- a/frontend/playwright/scripts/MockWebSocket.js +++ b/frontend/playwright/scripts/MockWebSocket.js @@ -11,20 +11,15 @@ window.WebSocket = class MockWebSocket extends EventTarget { } static getByURL(url) { - 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); - }); + if (this.#mocks.has(url)) { + return this.#mocks.get(url); + } + for (const [wsURL, ws] of this.#mocks) { + if (wsURL.includes(url)) { + return ws; + } + } + return undefined; } #url; diff --git a/frontend/playwright/workspace.spec.js b/frontend/playwright/workspace.spec.js index ff0bc5fee..07e8762ca 100644 --- a/frontend/playwright/workspace.spec.js +++ b/frontend/playwright/workspace.spec.js @@ -1,6 +1,6 @@ import { test, expect } from "@playwright/test"; import { interceptRPC, interceptRPCByRegex } from "./helpers/MockAPI"; -import { WebSocketManager } from "./helpers/MockWebSocket"; +import { MockWebSocket } from "./helpers/MockWebSocket"; import { presenceFixture } from "./fixtures/workspace/ws-notifications"; const anyProjectId = "c7ce0794-0992-8105-8004-38e630f7920b"; @@ -30,7 +30,7 @@ const setupWorkspaceUser = (page) => { }; test.beforeEach(async ({ page }) => { - await WebSocketManager.init(page); + await MockWebSocket.init(page); }); test("User loads worskpace with empty file", async ({ page }) => { @@ -45,13 +45,15 @@ 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(); - }); + const ws = await MockWebSocket.waitForURL("ws://0.0.0.0:3500/ws/notifications") + await ws.mockOpen(); + console.log('JEEEEEE', ws) await expect(page.getByTestId("page-name")).toHaveText("Page 1"); + await ws.mockMessage(JSON.stringify(presenceFixture)); + + /* await page.evaluate( async ({ presenceFixture }) => { const ws = await WebSocket.waitForURL("ws://0.0.0.0:3500/ws/notifications"); @@ -59,11 +61,16 @@ test.only("User receives notifications updates in the workspace", async ({ page }, { presenceFixture }, ); + */ - expect(page.getByTestId("active-users-list").getByAltText("Princesa Leia")).toHaveCount(2); + await expect(page.getByTestId("active-users-list").getByAltText("Princesa Leia")).toHaveCount(2); + await ws.mockClose(); + + /* await page.evaluate(async () => { const ws = await WebSocket.waitForURL("ws://0.0.0.0:3500/ws/notifications"); ws.mockClose(); }); + */ }); From 0091ac0f5f26971304567b134b5bd8907d1f1165 Mon Sep 17 00:00:00 2001 From: AzazelN28 Date: Tue, 7 May 2024 13:12:58 +0200 Subject: [PATCH 4/4] :recycle: Refactor tests and pages --- .../workspace/get-comment-threads-empty.json | 0 .../workspace/get-file-blank.json | 0 .../workspace/get-file-fragment-blank.json | 0 .../workspace/get-file-libraries-empty.json | 0 .../get-file-object-thumbnails-blank.json | 0 .../workspace/get-font-variants-empty.json | 0 .../get-profile-for-file-comments.json | 0 .../workspace/get-project-default.json | 0 .../workspace/get-team-default.json | 0 .../workspace/ws-notifications.js | 0 frontend/playwright/fixtures/login-fixture.js | 10 +++ frontend/playwright/helpers/MockAPI.js | 17 ----- ...ockWebSocket.js => MockWebSocketHelper.js} | 14 ++-- frontend/playwright/helpers/intercepts.js | 8 -- frontend/playwright/login.spec.js | 57 -------------- frontend/playwright/scripts/MockWebSocket.js | 5 -- frontend/playwright/ui/pages/BasePage.js | 38 ++++++++++ .../playwright/ui/pages/BaseWebSocketPage.js | 32 ++++++++ frontend/playwright/ui/pages/LoginPage.js | 54 +++++++++++++ frontend/playwright/ui/pages/login-page.js | 76 ------------------- frontend/playwright/ui/specs/login.spec.js | 13 ++-- .../playwright/ui/specs/workspace.spec.js | 54 +++++++++++++ frontend/playwright/workspace.spec.js | 76 ------------------- .../main/data/workspace/notifications.cljs | 1 - 24 files changed, 201 insertions(+), 254 deletions(-) rename frontend/playwright/{fixtures => data}/workspace/get-comment-threads-empty.json (100%) rename frontend/playwright/{fixtures => data}/workspace/get-file-blank.json (100%) rename frontend/playwright/{fixtures => data}/workspace/get-file-fragment-blank.json (100%) rename frontend/playwright/{fixtures => data}/workspace/get-file-libraries-empty.json (100%) rename frontend/playwright/{fixtures => data}/workspace/get-file-object-thumbnails-blank.json (100%) rename frontend/playwright/{fixtures => data}/workspace/get-font-variants-empty.json (100%) rename frontend/playwright/{fixtures => data}/workspace/get-profile-for-file-comments.json (100%) rename frontend/playwright/{fixtures => data}/workspace/get-project-default.json (100%) rename frontend/playwright/{fixtures => data}/workspace/get-team-default.json (100%) rename frontend/playwright/{fixtures => data}/workspace/ws-notifications.js (100%) create mode 100644 frontend/playwright/fixtures/login-fixture.js delete mode 100644 frontend/playwright/helpers/MockAPI.js rename frontend/playwright/helpers/{MockWebSocket.js => MockWebSocketHelper.js} (85%) delete mode 100644 frontend/playwright/helpers/intercepts.js delete mode 100644 frontend/playwright/login.spec.js create mode 100644 frontend/playwright/ui/pages/BasePage.js create mode 100644 frontend/playwright/ui/pages/BaseWebSocketPage.js create mode 100644 frontend/playwright/ui/pages/LoginPage.js delete mode 100644 frontend/playwright/ui/pages/login-page.js create mode 100644 frontend/playwright/ui/specs/workspace.spec.js delete mode 100644 frontend/playwright/workspace.spec.js diff --git a/frontend/playwright/fixtures/workspace/get-comment-threads-empty.json b/frontend/playwright/data/workspace/get-comment-threads-empty.json similarity index 100% rename from frontend/playwright/fixtures/workspace/get-comment-threads-empty.json rename to frontend/playwright/data/workspace/get-comment-threads-empty.json diff --git a/frontend/playwright/fixtures/workspace/get-file-blank.json b/frontend/playwright/data/workspace/get-file-blank.json similarity index 100% rename from frontend/playwright/fixtures/workspace/get-file-blank.json rename to frontend/playwright/data/workspace/get-file-blank.json diff --git a/frontend/playwright/fixtures/workspace/get-file-fragment-blank.json b/frontend/playwright/data/workspace/get-file-fragment-blank.json similarity index 100% rename from frontend/playwright/fixtures/workspace/get-file-fragment-blank.json rename to frontend/playwright/data/workspace/get-file-fragment-blank.json diff --git a/frontend/playwright/fixtures/workspace/get-file-libraries-empty.json b/frontend/playwright/data/workspace/get-file-libraries-empty.json similarity index 100% rename from frontend/playwright/fixtures/workspace/get-file-libraries-empty.json rename to frontend/playwright/data/workspace/get-file-libraries-empty.json diff --git a/frontend/playwright/fixtures/workspace/get-file-object-thumbnails-blank.json b/frontend/playwright/data/workspace/get-file-object-thumbnails-blank.json similarity index 100% rename from frontend/playwright/fixtures/workspace/get-file-object-thumbnails-blank.json rename to frontend/playwright/data/workspace/get-file-object-thumbnails-blank.json diff --git a/frontend/playwright/fixtures/workspace/get-font-variants-empty.json b/frontend/playwright/data/workspace/get-font-variants-empty.json similarity index 100% rename from frontend/playwright/fixtures/workspace/get-font-variants-empty.json rename to frontend/playwright/data/workspace/get-font-variants-empty.json diff --git a/frontend/playwright/fixtures/workspace/get-profile-for-file-comments.json b/frontend/playwright/data/workspace/get-profile-for-file-comments.json similarity index 100% rename from frontend/playwright/fixtures/workspace/get-profile-for-file-comments.json rename to frontend/playwright/data/workspace/get-profile-for-file-comments.json diff --git a/frontend/playwright/fixtures/workspace/get-project-default.json b/frontend/playwright/data/workspace/get-project-default.json similarity index 100% rename from frontend/playwright/fixtures/workspace/get-project-default.json rename to frontend/playwright/data/workspace/get-project-default.json diff --git a/frontend/playwright/fixtures/workspace/get-team-default.json b/frontend/playwright/data/workspace/get-team-default.json similarity index 100% rename from frontend/playwright/fixtures/workspace/get-team-default.json rename to frontend/playwright/data/workspace/get-team-default.json diff --git a/frontend/playwright/fixtures/workspace/ws-notifications.js b/frontend/playwright/data/workspace/ws-notifications.js similarity index 100% rename from frontend/playwright/fixtures/workspace/ws-notifications.js rename to frontend/playwright/data/workspace/ws-notifications.js diff --git a/frontend/playwright/fixtures/login-fixture.js b/frontend/playwright/fixtures/login-fixture.js new file mode 100644 index 000000000..866453793 --- /dev/null +++ b/frontend/playwright/fixtures/login-fixture.js @@ -0,0 +1,10 @@ +import { test as base } from '@playwright/test' + +export const test = base.extend({ + loginPage: async ({ page }, use) => { + const loginPage = new LoginPage(page) + await use(loginPage) + } +}) + +export { expect } from '@playwright/test' diff --git a/frontend/playwright/helpers/MockAPI.js b/frontend/playwright/helpers/MockAPI.js deleted file mode 100644 index 8a5e970c4..000000000 --- a/frontend/playwright/helpers/MockAPI.js +++ /dev/null @@ -1,17 +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}`, - }), - ); - -export const interceptRPCByRegex = (page, regex, jsonFilename) => - page.route(regex, (route) => - route.fulfill({ - status: 200, - contentType: "application/transit+json", - path: `playwright/fixtures/${jsonFilename}`, - }), - ); diff --git a/frontend/playwright/helpers/MockWebSocket.js b/frontend/playwright/helpers/MockWebSocketHelper.js similarity index 85% rename from frontend/playwright/helpers/MockWebSocket.js rename to frontend/playwright/helpers/MockWebSocketHelper.js index 1a6c8eb81..3f0f845d2 100644 --- a/frontend/playwright/helpers/MockWebSocket.js +++ b/frontend/playwright/helpers/MockWebSocketHelper.js @@ -1,18 +1,21 @@ -export class MockWebSocket extends EventTarget { +export class MockWebSocketHelper extends EventTarget { static #mocks = new Map(); 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); + const webSocket = new MockWebSocketHelper(page, url, protocols); this.#mocks.set(url, webSocket); }); await page.exposeFunction("MockWebSocket$$spyMessage", (url, data) => { - console.log("MockWebSocket$$spyMessage", url, data); + if (!this.#mocks.has(url)) { + throw new Error(`WebSocket with URL ${url} not found`); + } this.#mocks.get(url).dispatchEvent(new MessageEvent("message", { data })); }); await page.exposeFunction("MockWebSocket$$spyClose", (url, code, reason) => { - console.log("MockWebSocket$$spyClose", url, code, reason); + if (!this.#mocks.has(url)) { + throw new Error(`WebSocket with URL ${url} not found`); + } this.#mocks.get(url).dispatchEvent(new CloseEvent("close", { code, reason })); }); await page.addInitScript({ path: "playwright/scripts/MockWebSocket.js" }); @@ -22,7 +25,6 @@ export class MockWebSocket extends EventTarget { return new Promise((resolve) => { const intervalID = setInterval(() => { for (const [wsURL, ws] of this.#mocks) { - console.log('waitForURL', wsURL); if (wsURL.includes(url)) { clearInterval(intervalID); return resolve(ws); diff --git a/frontend/playwright/helpers/intercepts.js b/frontend/playwright/helpers/intercepts.js deleted file mode 100644 index ecb46b817..000000000 --- a/frontend/playwright/helpers/intercepts.js +++ /dev/null @@ -1,8 +0,0 @@ -import { interceptRPC } from "./index"; - - -export const setupNotLogedIn = async (page) => { - await interceptRPC(page, "get-profile", "get-profile-anonymous.json"); - -}; - diff --git a/frontend/playwright/login.spec.js b/frontend/playwright/login.spec.js deleted file mode 100644 index cade62385..000000000 --- a/frontend/playwright/login.spec.js +++ /dev/null @@ -1,57 +0,0 @@ -import { test, expect } from "@playwright/test"; -import { interceptRPC } from "./helpers/MockAPI"; - -const setupLoggedOutUser = async (page) => { - await interceptRPC(page, "get-profile", "get-profile-anonymous.json"); - await interceptRPC(page, "login-with-password", "logged-in-user/login-with-password-success.json"); -}; - -// TODO: maybe Playwright's fixtures are the right way to do this? -const setupDashboardUser = async (page) => { - await interceptRPC(page, "get-profile", "logged-in-user/get-profile-logged-in.json"); - await interceptRPC(page, "get-teams", "logged-in-user/get-teams-default.json"); - await interceptRPC(page, "get-font-variants?team-id=*", "logged-in-user/get-font-variants-empty.json"); - await interceptRPC(page, "get-projects?team-id=*", "logged-in-user/get-projects-default.json"); - await interceptRPC(page, "get-team-members?team-id=*", "logged-in-user/get-team-members-your-penpot.json"); - await interceptRPC(page, "get-team-users?team-id=*", "logged-in-user/get-team-users-single-user.json"); - await interceptRPC( - page, - "get-unread-comment-threads?team-id=*", - "logged-in-user/get-team-users-single-user.json", - ); - await interceptRPC( - page, - "get-team-recent-files?team-id=*", - "logged-in-user/get-team-recent-files-empty.json", - ); - await interceptRPC( - 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"); -}; - -test("Shows login page when going to index and user is logged out", async ({ page }) => { - await setupLoggedOutUser(page); - - await page.goto("/"); - - await expect(page).toHaveURL(/auth\/login$/); - await expect(page.getByText("Log into my account")).toBeVisible(); -}); - -test("User logs in by filling the login form", async ({ page }) => { - await setupLoggedOutUser(page); - - await page.goto("/#/auth/login"); - - await setupDashboardUser(page); - - await page.getByLabel("Email").fill("foo@example.com"); - await page.getByLabel("Password").fill("loremipsum"); - - await page.getByRole("button", { name: "Login" }).click(); - - await expect(page).toHaveURL(/dashboard/); -}); diff --git a/frontend/playwright/scripts/MockWebSocket.js b/frontend/playwright/scripts/MockWebSocket.js index 60723eb29..b7f5e4e30 100644 --- a/frontend/playwright/scripts/MockWebSocket.js +++ b/frontend/playwright/scripts/MockWebSocket.js @@ -39,7 +39,6 @@ window.WebSocket = class MockWebSocket extends EventTarget { #spyClose = null; constructor(url, protocols) { - console.log("🤖 New websocket at", url); super(); this.#url = url; @@ -166,7 +165,6 @@ window.WebSocket = class MockWebSocket extends EventTarget { } mockOpen(options) { - console.log("🤖 open mock"); this.#protocol = options?.protocol || ""; this.#extensions = options?.extensions || ""; this.#readyState = MockWebSocket.OPEN; @@ -181,12 +179,9 @@ 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; } diff --git a/frontend/playwright/ui/pages/BasePage.js b/frontend/playwright/ui/pages/BasePage.js new file mode 100644 index 000000000..e0e62a0a1 --- /dev/null +++ b/frontend/playwright/ui/pages/BasePage.js @@ -0,0 +1,38 @@ +export class BasePage { + static async mockRPC(page, path, jsonFilename, options) { + if (!page) { + throw new TypeError("Invalid page argument. Must be a Playwright page."); + } + if (typeof path !== "string" && !(path instanceof RegExp)) { + throw new TypeError("Invalid path argument. Must be a string or a RegExp."); + } + const url = typeof path === "string" ? `**/api/rpc/command/${path}` : path; + const interceptConfig = { + status: 200, + contentType: "application/transit+json", + ...options, + }; + return page.route(url, (route) => + route.fulfill({ + ...interceptConfig, + path: `playwright/data/${jsonFilename}`, + }), + ); + } + + #page = null; + + constructor(page) { + this.#page = page; + } + + get page() { + return this.#page; + } + + async mockRPC(path, jsonFilename, options) { + return BasePage.mockRPC(this.page, path, jsonFilename, options); + } +} + +export default BasePage; diff --git a/frontend/playwright/ui/pages/BaseWebSocketPage.js b/frontend/playwright/ui/pages/BaseWebSocketPage.js new file mode 100644 index 000000000..f76a7f5c4 --- /dev/null +++ b/frontend/playwright/ui/pages/BaseWebSocketPage.js @@ -0,0 +1,32 @@ +import { MockWebSocketHelper } from "../../helpers/MockWebSocketHelper"; +import BasePage from "./BasePage"; + +export class BaseWebSocketPage extends BasePage { + /** + * This should be called on `test.beforeEach`. + * + * @param {Page} page + * @returns + */ + static setupWebSockets(page) { + return MockWebSocketHelper.init(page); + } + + /** + * Returns a promise that resolves when a WebSocket with the given URL is created. + * + * @param {string} url + * @returns {Promise} + */ + async waitForWebSocket(url) { + return MockWebSocketHelper.waitForURL(url); + } + + /** + * + * @returns {Promise} + */ + async waitForNotificationsWebSocket() { + return this.waitForWebSocket("ws://0.0.0.0:3500/ws/notifications"); + } +} diff --git a/frontend/playwright/ui/pages/LoginPage.js b/frontend/playwright/ui/pages/LoginPage.js new file mode 100644 index 000000000..e0acf8b12 --- /dev/null +++ b/frontend/playwright/ui/pages/LoginPage.js @@ -0,0 +1,54 @@ +import { BasePage } from "./BasePage"; + +export class LoginPage extends BasePage { + static setupLoggedOutUser(page) { + return this.mockRPC(page, "get-profile", "get-profile-anonymous.json"); + } + + constructor(page) { + super(page); + this.loginButton = page.getByRole("button", { name: "Login" }); + this.password = page.getByLabel("Password"); + this.userName = page.getByLabel("Email"); + this.message = page.getByText("Email or password is incorrect"); + this.badLoginMsg = page.getByText("Enter a valid email please"); + this.initialHeading = page.getByRole("heading", { name: "Log into my account" }); + } + + async fillEmailAndPasswordInputs(email, password) { + await this.userName.fill(email); + await this.password.fill(password); + } + + async clickLoginButton() { + await this.loginButton.click(); + } + + async setupAllowedUser() { + await this.mockRPC("get-profile", "logged-in-user/get-profile-logged-in.json"); + await this.mockRPC("get-teams", "logged-in-user/get-teams-default.json"); + await this.mockRPC("get-font-variants?team-id=*", "logged-in-user/get-font-variants-empty.json"); + await this.mockRPC("get-projects?team-id=*", "logged-in-user/get-projects-default.json"); + await this.mockRPC("get-team-members?team-id=*", "logged-in-user/get-team-members-your-penpot.json"); + await this.mockRPC("get-team-users?team-id=*", "logged-in-user/get-team-users-single-user.json"); + await this.mockRPC( + "get-unread-comment-threads?team-id=*", + "logged-in-user/get-team-users-single-user.json", + ); + await this.mockRPC("get-team-recent-files?team-id=*", "logged-in-user/get-team-recent-files-empty.json"); + await this.mockRPC( + "get-profiles-for-file-comments", + "logged-in-user/get-profiles-for-file-comments-empty.json", + ); + } + + async setupLoginSuccess() { + await this.mockRPC("login-with-password", "logged-in-user/login-with-password-success.json"); + } + + async setupLoginError() { + await this.mockRPC("login-with-password", "login-with-password-error.json", { status: 400 }); + } +} + +export default LoginPage; diff --git a/frontend/playwright/ui/pages/login-page.js b/frontend/playwright/ui/pages/login-page.js deleted file mode 100644 index 1358f4ab3..000000000 --- a/frontend/playwright/ui/pages/login-page.js +++ /dev/null @@ -1,76 +0,0 @@ -import { interceptRPC } from "../../helpers/index"; - -class LoginPage { - constructor(page) { - this.page = page; - this.loginButton = page.getByRole("button", { name: "Login" }); - this.password = page.getByLabel("Password"); - this.userName = page.getByLabel("Email"); - this.message = page.getByText("Email or password is incorrect"); - this.badLoginMsg = page.getByText("Enter a valid email please"); - this.initialHeading = page.getByRole("heading", { name: "Log into my account" }); - } - - url() { - return this.page.url(); - } - - context() { - return this.page.context(); - } - - async fillEmailAndPasswordInputs(email, password) { - await this.userName.fill(email); - await this.password.fill(password); - } - - async clickLoginButton() { - await this.loginButton.click(); - } - - async setupAllowedUser() { - await interceptRPC(this.page, "get-profile", "logged-in-user/get-profile-logged-in.json"); - await interceptRPC(this.page, "get-teams", "logged-in-user/get-teams-default.json"); - await interceptRPC( - this.page, - "get-font-variants?team-id=*", - "logged-in-user/get-font-variants-empty.json", - ); - await interceptRPC(this.page, "get-projects?team-id=*", "logged-in-user/get-projects-default.json"); - await interceptRPC( - this.page, - "get-team-members?team-id=*", - "logged-in-user/get-team-members-your-penpot.json", - ); - await interceptRPC( - this.page, - "get-team-users?team-id=*", - "logged-in-user/get-team-users-single-user.json", - ); - await interceptRPC( - this.page, - "get-unread-comment-threads?team-id=*", - "logged-in-user/get-team-users-single-user.json", - ); - await interceptRPC( - this.page, - "get-team-recent-files?team-id=*", - "logged-in-user/get-team-recent-files-empty.json", - ); - await interceptRPC( - this.page, - "get-profiles-for-file-comments", - "logged-in-user/get-profiles-for-file-comments-empty.json", - ); - } - - async setupLoginSuccess() { - await interceptRPC(this.page, "login-with-password", "logged-in-user/login-with-password-success.json"); - } - - async setupLoginError() { - await interceptRPC(this.page, "login-with-password", "login-with-password-error.json", { status: 400 }); - } -} - -export default LoginPage; diff --git a/frontend/playwright/ui/specs/login.spec.js b/frontend/playwright/ui/specs/login.spec.js index 08d3753c4..dab5a5ca6 100644 --- a/frontend/playwright/ui/specs/login.spec.js +++ b/frontend/playwright/ui/specs/login.spec.js @@ -1,10 +1,8 @@ import { test, expect } from "@playwright/test"; -import { setupNotLogedIn } from "../../helpers/intercepts"; - -import LoginPage from "../pages/login-page"; +import { LoginPage } from "../pages/LoginPage"; test.beforeEach(async ({ page }) => { - await setupNotLogedIn(page); + await LoginPage.setupLoggedOutUser(page); await page.goto("/#/auth/login"); }); @@ -13,7 +11,7 @@ test("Shows login page when going to index and user is logged out", async ({ pag await loginPage.setupAllowedUser(); - await expect(loginPage.url()).toMatch(/auth\/login$/); + await expect(loginPage.page).toHaveURL(/auth\/login$/); await expect(loginPage.initialHeading).toBeVisible(); }); @@ -37,8 +35,7 @@ test("User logs in by filling the login form", async ({ page }) => { await loginPage.clickLoginButton(); await page.waitForURL('**/dashboard/**'); - await expect(page).toHaveURL(/dashboard/); - // await expect(loginPage.url()).toMatch(/dashboard/); + await expect(loginPage.page).toHaveURL(/dashboard/); }); test("User submits wrong credentials", async ({ page }) => { @@ -50,5 +47,5 @@ test("User submits wrong credentials", async ({ page }) => { await loginPage.clickLoginButton(); await expect(loginPage.message).toBeVisible(); - await expect(loginPage.url()).toMatch(/auth\/login$/); + await expect(loginPage.page).toHaveURL(/auth\/login$/); }); diff --git a/frontend/playwright/ui/specs/workspace.spec.js b/frontend/playwright/ui/specs/workspace.spec.js new file mode 100644 index 000000000..3f24e2d28 --- /dev/null +++ b/frontend/playwright/ui/specs/workspace.spec.js @@ -0,0 +1,54 @@ +import { test, expect } from "@playwright/test"; +import { BasePage } from "../pages/BasePage"; +import { MockWebSocketHelper } from "../../helpers/MockWebSocketHelper"; +import { presenceFixture } from "../../data/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) => { + BasePage.mockRPC(page, "get-profile", "logged-in-user/get-profile-logged-in.json"); + BasePage.mockRPC(page, "get-team-users?file-id=*", "logged-in-user/get-team-users-single-user.json"); + BasePage.mockRPC(page, "get-comment-threads?file-id=*", "workspace/get-comment-threads-empty.json"); + BasePage.mockRPC(page, "get-project?id=*", "workspace/get-project-default.json"); + BasePage.mockRPC(page, "get-team?id=*", "workspace/get-team-default.json"); + BasePage.mockRPC(page, /get\-file\?/, "workspace/get-file-blank.json"); + BasePage.mockRPC( + page, + "get-file-object-thumbnails?file-id=*", + "workspace/get-file-object-thumbnails-blank.json", + ); + BasePage.mockRPC( + page, + "get-profiles-for-file-comments?file-id=*", + "workspace/get-profile-for-file-comments.json", + ); + BasePage.mockRPC(page, "get-font-variants?team-id=*", "workspace/get-font-variants-empty.json"); + BasePage.mockRPC(page, "get-file-fragment?file-id=*", "workspace/get-file-fragment-blank.json"); + BasePage.mockRPC(page, "get-file-libraries?file-id=*", "workspace/get-file-libraries-empty.json"); +}; + +test.beforeEach(async ({ page }) => { + await MockWebSocketHelper.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("User receives notifications updates in the workspace", async ({ page }) => { + await setupWorkspaceUser(page); + await page.goto(`/#/workspace/${anyProjectId}/${anyFileId}?page-id=${anyPageId}`); + + const ws = await MockWebSocketHelper.waitForURL("ws://0.0.0.0:3500/ws/notifications") + await ws.mockOpen(); + await expect(page.getByTestId("page-name")).toHaveText("Page 1"); + await ws.mockMessage(JSON.stringify(presenceFixture)); + await expect(page.getByTestId("active-users-list").getByAltText("Princesa Leia")).toHaveCount(2); + await ws.mockClose(); +}); diff --git a/frontend/playwright/workspace.spec.js b/frontend/playwright/workspace.spec.js deleted file mode 100644 index 07e8762ca..000000000 --- a/frontend/playwright/workspace.spec.js +++ /dev/null @@ -1,76 +0,0 @@ -import { test, expect } from "@playwright/test"; -import { interceptRPC, interceptRPCByRegex } from "./helpers/MockAPI"; -import { MockWebSocket } 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 MockWebSocket.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}`); - - const ws = await MockWebSocket.waitForURL("ws://0.0.0.0:3500/ws/notifications") - await ws.mockOpen(); - console.log('JEEEEEE', ws) - - await expect(page.getByTestId("page-name")).toHaveText("Page 1"); - - await ws.mockMessage(JSON.stringify(presenceFixture)); - - /* - await page.evaluate( - async ({ presenceFixture }) => { - const ws = await WebSocket.waitForURL("ws://0.0.0.0:3500/ws/notifications"); - ws.mockMessage(JSON.stringify(presenceFixture)); - }, - { presenceFixture }, - ); - */ - - await expect(page.getByTestId("active-users-list").getByAltText("Princesa Leia")).toHaveCount(2); - - await ws.mockClose(); - - /* - await page.evaluate(async () => { - const ws = await WebSocket.waitForURL("ws://0.0.0.0:3500/ws/notifications"); - ws.mockClose(); - }); - */ -}); diff --git a/frontend/src/app/main/data/workspace/notifications.cljs b/frontend/src/app/main/data/workspace/notifications.cljs index b721c2485..4bb3a9772 100644 --- a/frontend/src/app/main/data/workspace/notifications.cljs +++ b/frontend/src/app/main/data/workspace/notifications.cljs @@ -162,7 +162,6 @@ (assoc :text-color "#000000"))) (update-presence [presence] - (js/console.log "🥰 WIIIIII" (clj->js presence)) (-> presence (update session-id update-session presence) (d/without-nils)))]