mirror of
https://github.com/penpot/penpot.git
synced 2025-05-15 15:36:37 +02:00
📎 Add WebSocket mock
This commit is contained in:
parent
38e35fb5ae
commit
30321e54f0
11 changed files with 589 additions and 24 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -68,3 +68,7 @@
|
||||||
clj-profiler/
|
clj-profiler/
|
||||||
node_modules
|
node_modules
|
||||||
frontend/.storybook/preview-body.html
|
frontend/.storybook/preview-body.html
|
||||||
|
/test-results/
|
||||||
|
/playwright-report/
|
||||||
|
/blob-report/
|
||||||
|
/playwright/.cache/
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
[]
|
8
frontend/playwright/helpers/MockRPC.js
Normal file
8
frontend/playwright/helpers/MockRPC.js
Normal file
|
@ -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}`,
|
||||||
|
})
|
||||||
|
);
|
81
frontend/playwright/helpers/MockWebSocket.js
Normal file
81
frontend/playwright/helpers/MockWebSocket.js
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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}`,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
69
frontend/playwright/login.spec.js
Normal file
69
frontend/playwright/login.spec.js
Normal file
|
@ -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/);
|
||||||
|
});
|
220
frontend/playwright/scripts/MockWebSocket.js
Normal file
220
frontend/playwright/scripts/MockWebSocket.js
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
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:backend": "clj-kondo --parallel=true --lint backend/src",
|
||||||
"lint:clj:exporter": "clj-kondo --parallel=true --lint exporter/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"
|
"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.
|
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||||
# Manual changes might be lost - proceed with caution!
|
# yarn lockfile v1
|
||||||
|
|
||||||
__metadata:
|
|
||||||
version: 8
|
|
||||||
cacheKey: 10c0
|
|
||||||
|
|
||||||
"penpot@workspace:.":
|
"@playwright/test@^1.43.1":
|
||||||
version: 0.0.0-use.local
|
version "1.43.1"
|
||||||
resolution: "penpot@workspace:."
|
resolved "https://registry.npmjs.org/@playwright/test/-/test-1.43.1.tgz"
|
||||||
languageName: unknown
|
integrity sha512-HgtQzFgNEEo4TE22K/X7sYTYNqEMMTZmFS8kTq6m8hXj+m1D8TgwgIbumHddJa9h4yl4GkKb8/bgAl2+g7eDgA==
|
||||||
linkType: soft
|
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
Add a link
Reference in a new issue