mirror of
https://github.com/penpot/penpot.git
synced 2025-04-11 06:21:30 -05:00
Merge pull request #4557 from penpot/azazeln28-test-add-websocket-mock
✨ Add basic test with websocket mock
This commit is contained in:
commit
127c47a35a
29 changed files with 915 additions and 119 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -68,3 +68,7 @@
|
|||
clj-profiler/
|
||||
node_modules
|
||||
frontend/.storybook/preview-body.html
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
[]
|
|
@ -0,0 +1 @@
|
|||
[]
|
58
frontend/playwright/data/workspace/get-file-blank.json
Normal file
58
frontend/playwright/data/workspace/get-file-blank.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
[]
|
|
@ -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"
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
[]
|
|
@ -0,0 +1,9 @@
|
|||
[
|
||||
{
|
||||
"~:id": "~uc7ce0794-0992-8105-8004-38e630f29a9b",
|
||||
"~:email": "foo@example.com",
|
||||
"~:name": "Princesa Leia",
|
||||
"~:fullname": "Princesa Leia",
|
||||
"~:is-active": true
|
||||
}
|
||||
]
|
|
@ -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"
|
||||
}
|
23
frontend/playwright/data/workspace/get-team-default.json
Normal file
23
frontend/playwright/data/workspace/get-team-default.json
Normal 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
|
||||
}
|
7
frontend/playwright/data/workspace/ws-notifications.js
Normal file
7
frontend/playwright/data/workspace/ws-notifications.js
Normal 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",
|
||||
};
|
10
frontend/playwright/fixtures/login-fixture.js
Normal file
10
frontend/playwright/fixtures/login-fixture.js
Normal file
|
@ -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'
|
74
frontend/playwright/helpers/MockWebSocketHelper.js
Normal file
74
frontend/playwright/helpers/MockWebSocketHelper.js
Normal file
|
@ -0,0 +1,74 @@
|
|||
export class MockWebSocketHelper extends EventTarget {
|
||||
static #mocks = new Map();
|
||||
|
||||
static async init(page) {
|
||||
await page.exposeFunction("MockWebSocket$$constructor", (url, protocols) => {
|
||||
const webSocket = new MockWebSocketHelper(page, url, protocols);
|
||||
this.#mocks.set(url, webSocket);
|
||||
});
|
||||
await page.exposeFunction("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) => {
|
||||
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" });
|
||||
}
|
||||
|
||||
static waitForURL(url) {
|
||||
return new Promise((resolve) => {
|
||||
const intervalID = setInterval(() => {
|
||||
for (const [wsURL, ws] of this.#mocks) {
|
||||
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 });
|
||||
}
|
||||
}
|
|
@ -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}`,
|
||||
});
|
||||
});
|
||||
};
|
|
@ -1,8 +0,0 @@
|
|||
import { interceptRPC } from "./index";
|
||||
|
||||
|
||||
export const setupNotLogedIn = async (page) => {
|
||||
await interceptRPC(page, "get-profile", "get-profile-anonymous.json");
|
||||
|
||||
};
|
||||
|
226
frontend/playwright/scripts/MockWebSocket.js
Normal file
226
frontend/playwright/scripts/MockWebSocket.js
Normal file
|
@ -0,0 +1,226 @@
|
|||
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) {
|
||||
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;
|
||||
#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();
|
||||
|
||||
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) {
|
||||
this.removeEventListener("open", this.#onopen);
|
||||
this.#onopen = null;
|
||||
|
||||
if (typeof callback === "function") {
|
||||
this.addEventListener("open", callback);
|
||||
this.#onopen = callback;
|
||||
}
|
||||
}
|
||||
|
||||
get onopen() {
|
||||
return this.#onopen;
|
||||
}
|
||||
|
||||
set onerror(callback) {
|
||||
this.removeEventListener("error", this.#onerror);
|
||||
this.#onerror = null;
|
||||
|
||||
if (typeof callback === "function") {
|
||||
this.addEventListener("error", callback);
|
||||
this.#onerror = callback;
|
||||
}
|
||||
}
|
||||
|
||||
get onerror() {
|
||||
return this.#onerror;
|
||||
}
|
||||
|
||||
set onmessage(callback) {
|
||||
this.removeEventListener("message", this.#onmessage);
|
||||
this.#onmessage = null;
|
||||
|
||||
if (typeof callback === "function") {
|
||||
this.addEventListener("message", callback);
|
||||
this.#onmessage = callback;
|
||||
}
|
||||
}
|
||||
|
||||
get onmessage() {
|
||||
return this.#onmessage;
|
||||
}
|
||||
|
||||
set onclose(callback) {
|
||||
this.removeEventListener("close", this.#onclose);
|
||||
this.#onclose = null;
|
||||
|
||||
if (typeof callback === "function") {
|
||||
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) {
|
||||
if (this.#readyState === MockWebSocket.CONNECTING) {
|
||||
throw new DOMException("InvalidStateError", "MockWebSocket is not connected");
|
||||
}
|
||||
|
||||
if (this.#spyMessage) {
|
||||
this.#spyMessage(this.url, data);
|
||||
}
|
||||
}
|
||||
|
||||
close(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;
|
||||
if (this.#spyClose) {
|
||||
this.#spyClose(this.url, code, reason);
|
||||
}
|
||||
}
|
||||
};
|
38
frontend/playwright/ui/pages/BasePage.js
Normal file
38
frontend/playwright/ui/pages/BasePage.js
Normal file
|
@ -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;
|
32
frontend/playwright/ui/pages/BaseWebSocketPage.js
Normal file
32
frontend/playwright/ui/pages/BaseWebSocketPage.js
Normal file
|
@ -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<MockWebSocketHelper>}
|
||||
*/
|
||||
async waitForWebSocket(url) {
|
||||
return MockWebSocketHelper.waitForURL(url);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {Promise<MockWebSocketHelper>}
|
||||
*/
|
||||
async waitForNotificationsWebSocket() {
|
||||
return this.waitForWebSocket("ws://0.0.0.0:3500/ws/notifications");
|
||||
}
|
||||
}
|
54
frontend/playwright/ui/pages/LoginPage.js
Normal file
54
frontend/playwright/ui/pages/LoginPage.js
Normal file
|
@ -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;
|
|
@ -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;
|
|
@ -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$/);
|
||||
});
|
||||
|
|
54
frontend/playwright/ui/specs/workspace.spec.js
Normal file
54
frontend/playwright/ui/specs/workspace.spec.js
Normal file
|
@ -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();
|
||||
});
|
|
@ -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))])
|
||||
|
||||
|
|
|
@ -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?))
|
||||
|
|
91
package-lock.json
generated
Normal file
91
package-lock.json
generated
Normal file
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
77
playwright.config.ts
Normal file
77
playwright.config.ts
Normal file
|
@ -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,
|
||||
// },
|
||||
});
|
44
yarn.lock
44
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==
|
||||
|
|
Loading…
Add table
Reference in a new issue