From 9fd9e0178e2a8cb5091e2a239306756c198742f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Wed, 8 May 2024 12:14:56 +0200 Subject: [PATCH 1/3] :recycle: Refactor LoginPage POM --- frontend/playwright/fixtures/login-fixture.js | 10 --- frontend/playwright/ui/pages/BasePage.js | 1 + frontend/playwright/ui/pages/LoginPage.js | 10 +-- frontend/playwright/ui/specs/login.spec.js | 65 +++++++++---------- .../playwright/ui/specs/workspace.spec.js | 38 +++++------ 5 files changed, 53 insertions(+), 71 deletions(-) delete mode 100644 frontend/playwright/fixtures/login-fixture.js diff --git a/frontend/playwright/fixtures/login-fixture.js b/frontend/playwright/fixtures/login-fixture.js deleted file mode 100644 index 866453793..000000000 --- a/frontend/playwright/fixtures/login-fixture.js +++ /dev/null @@ -1,10 +0,0 @@ -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/ui/pages/BasePage.js b/frontend/playwright/ui/pages/BasePage.js index e0e62a0a1..076bf13f6 100644 --- a/frontend/playwright/ui/pages/BasePage.js +++ b/frontend/playwright/ui/pages/BasePage.js @@ -6,6 +6,7 @@ export class BasePage { 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, diff --git a/frontend/playwright/ui/pages/LoginPage.js b/frontend/playwright/ui/pages/LoginPage.js index e0acf8b12..5e94c10ca 100644 --- a/frontend/playwright/ui/pages/LoginPage.js +++ b/frontend/playwright/ui/pages/LoginPage.js @@ -1,8 +1,8 @@ import { BasePage } from "./BasePage"; export class LoginPage extends BasePage { - static setupLoggedOutUser(page) { - return this.mockRPC(page, "get-profile", "get-profile-anonymous.json"); + static async initWithLoggedOutUser(page) { + await BasePage.mockRPC(page, "get-profile", "get-profile-anonymous.json"); } constructor(page) { @@ -10,8 +10,8 @@ export class LoginPage extends BasePage { 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.invalidCredentialsError = page.getByText("Email or password is incorrect"); + this.invalidEmailError = page.getByText("Enter a valid email please"); this.initialHeading = page.getByRole("heading", { name: "Log into my account" }); } @@ -24,7 +24,7 @@ export class LoginPage extends BasePage { await this.loginButton.click(); } - async setupAllowedUser() { + async setupLoggedInUser() { 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"); diff --git a/frontend/playwright/ui/specs/login.spec.js b/frontend/playwright/ui/specs/login.spec.js index dab5a5ca6..dd259cf77 100644 --- a/frontend/playwright/ui/specs/login.spec.js +++ b/frontend/playwright/ui/specs/login.spec.js @@ -2,50 +2,49 @@ import { test, expect } from "@playwright/test"; import { LoginPage } from "../pages/LoginPage"; test.beforeEach(async ({ page }) => { - await LoginPage.setupLoggedOutUser(page); + await LoginPage.initWithLoggedOutUser(page); await page.goto("/#/auth/login"); }); -test("Shows login page when going to index and user is logged out", async ({ page }) => { +test("User is redirected to the login page when logged out", async ({ page }) => { const loginPage = new LoginPage(page); - await loginPage.setupAllowedUser(); + await loginPage.setupLoggedInUser(); await expect(loginPage.page).toHaveURL(/auth\/login$/); await expect(loginPage.initialHeading).toBeVisible(); }); -test("User submit a wrong formated email ", async ({ page }) => { - const loginPage = new LoginPage(page); +test.describe("Login form", () => { + test("User logs in by filling the login form", async ({ page }) => { + const loginPage = new LoginPage(page); + await loginPage.setupLoginSuccess(); + await loginPage.setupLoggedInUser(); - await loginPage.setupLoginSuccess(); + await loginPage.fillEmailAndPasswordInputs("foo@example.com", "loremipsum"); + await loginPage.clickLoginButton(); - await loginPage.fillEmailAndPasswordInputs("foo", "lorenIpsum"); + await page.waitForURL("**/dashboard/**"); + await expect(loginPage.page).toHaveURL(/dashboard/); + }); - await expect(loginPage.badLoginMsg).toBeVisible(); -}); - -test("User logs in by filling the login form", async ({ page }) => { - const loginPage = new LoginPage(page); - - await loginPage.setupLoginSuccess(); - await loginPage.setupAllowedUser(); - - await loginPage.fillEmailAndPasswordInputs("foo@example.com", "loremipsum"); - await loginPage.clickLoginButton(); - - await page.waitForURL('**/dashboard/**'); - await expect(loginPage.page).toHaveURL(/dashboard/); -}); - -test("User submits wrong credentials", async ({ page }) => { - const loginPage = new LoginPage(page); - - await loginPage.setupLoginError(); - - await loginPage.fillEmailAndPasswordInputs("test@example.com", "loremipsum"); - await loginPage.clickLoginButton(); - - await expect(loginPage.message).toBeVisible(); - await expect(loginPage.page).toHaveURL(/auth\/login$/); + test("User gets error message when submitting an bad formatted email ", async ({ page }) => { + const loginPage = new LoginPage(page); + await loginPage.setupLoginSuccess(); + + await loginPage.fillEmailAndPasswordInputs("foo", "lorenIpsum"); + + await expect(loginPage.invalidEmailError).toBeVisible(); + }); + + test("User gets error message when submitting wrong credentials", async ({ page }) => { + const loginPage = new LoginPage(page); + await loginPage.setupLoginError(); + + await loginPage.fillEmailAndPasswordInputs("test@example.com", "loremipsum"); + await loginPage.clickLoginButton(); + + await expect(loginPage.invalidCredentialsError).toBeVisible(); + 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 index 3f24e2d28..57a60e979 100644 --- a/frontend/playwright/ui/specs/workspace.spec.js +++ b/frontend/playwright/ui/specs/workspace.spec.js @@ -1,5 +1,5 @@ import { test, expect } from "@playwright/test"; -import { BasePage } from "../pages/BasePage"; +import { BaseWebSocketPage } from "../pages/BaseWebSocketPage"; import { MockWebSocketHelper } from "../../helpers/MockWebSocketHelper"; import { presenceFixture } from "../../data/workspace/ws-notifications"; @@ -8,32 +8,24 @@ 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"); + page.mockRPC("get-profile", "logged-in-user/get-profile-logged-in.json"); + page.mockRPC("get-team-users?file-id=*", "logged-in-user/get-team-users-single-user.json"); + page.mockRPC("get-comment-threads?file-id=*", "workspace/get-comment-threads-empty.json"); + page.mockRPC("get-project?id=*", "workspace/get-project-default.json"); + page.mockRPC("get-team?id=*", "workspace/get-team-default.json"); + page.mockRPC(/get\-file\?/, "workspace/get-file-blank.json"); + page.mockRPC("get-file-object-thumbnails?file-id=*", "workspace/get-file-object-thumbnails-blank.json"); + page.mockRPC("get-profiles-for-file-comments?file-id=*", "workspace/get-profile-for-file-comments.json"); + page.mockRPC("get-font-variants?team-id=*", "workspace/get-font-variants-empty.json"); + page.mockRPC("get-file-fragment?file-id=*", "workspace/get-file-fragment-blank.json"); + page.mockRPC("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 }) => { +test.skip("User loads worskpace with empty file", async ({ page }) => { await setupWorkspaceUser(page); await page.goto(`/#/workspace/${anyProjectId}/${anyFileId}?page-id=${anyPageId}`); @@ -41,11 +33,11 @@ test("User loads worskpace with empty file", async ({ page }) => { await expect(page.getByTestId("page-name")).toHaveText("Page 1"); }); -test("User receives notifications updates in the workspace", async ({ page }) => { +test.skip("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") + 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)); From e28d56e670910de5a81cc9ff269758279f59a9eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Wed, 8 May 2024 14:22:18 +0200 Subject: [PATCH 2/3] :sparkles: Add WorkspacePage POM for playwright testing --- .../playwright/ui/pages/BaseWebSocketPage.js | 2 +- frontend/playwright/ui/pages/WorkspacePage.js | 89 +++++++++++++++++++ .../playwright/ui/specs/workspace.spec.js | 46 +++------- 3 files changed, 103 insertions(+), 34 deletions(-) create mode 100644 frontend/playwright/ui/pages/WorkspacePage.js diff --git a/frontend/playwright/ui/pages/BaseWebSocketPage.js b/frontend/playwright/ui/pages/BaseWebSocketPage.js index f76a7f5c4..85700e048 100644 --- a/frontend/playwright/ui/pages/BaseWebSocketPage.js +++ b/frontend/playwright/ui/pages/BaseWebSocketPage.js @@ -8,7 +8,7 @@ export class BaseWebSocketPage extends BasePage { * @param {Page} page * @returns */ - static setupWebSockets(page) { + static initWebSockets(page) { return MockWebSocketHelper.init(page); } diff --git a/frontend/playwright/ui/pages/WorkspacePage.js b/frontend/playwright/ui/pages/WorkspacePage.js new file mode 100644 index 000000000..f89c57a7a --- /dev/null +++ b/frontend/playwright/ui/pages/WorkspacePage.js @@ -0,0 +1,89 @@ +import { expect } from "@playwright/test"; +import { BaseWebSocketPage } from "./BaseWebSocketPage"; + +export class WorkspacePage extends BaseWebSocketPage { + /** + * This should be called on `test.beforeEach`. + * + * @param {Page} page + * @returns + */ + static async init(page) { + await BaseWebSocketPage.initWebSockets(page); + + await BaseWebSocketPage.mockRPC(page, "get-profile", "logged-in-user/get-profile-logged-in.json"); + await BaseWebSocketPage.mockRPC( + page, + "get-team-users?file-id=*", + "logged-in-user/get-team-users-single-user.json", + ); + await BaseWebSocketPage.mockRPC( + page, + "get-comment-threads?file-id=*", + "workspace/get-comment-threads-empty.json", + ); + await BaseWebSocketPage.mockRPC(page, "get-project?id=*", "workspace/get-project-default.json"); + await BaseWebSocketPage.mockRPC(page, "get-team?id=*", "workspace/get-team-default.json"); + await BaseWebSocketPage.mockRPC( + page, + "get-profiles-for-file-comments?file-id=*", + "workspace/get-profile-for-file-comments.json", + ); + } + + static anyProjectId = "c7ce0794-0992-8105-8004-38e630f7920b"; + static anyFileId = "c7ce0794-0992-8105-8004-38f280443849"; + static anyPageId = "c7ce0794-0992-8105-8004-38f28044384a"; + + #ws = null; + + constructor(page) { + super(page); + // TODO: add locators + this.pageName = page.getByTestId("page-name"); + this.presentUserListItems = page.getByTestId("active-users-list").getByAltText("Princesa Leia"); + } + + async goToWorkspace() { + await this.page.goto( + `/#/workspace/${WorkspacePage.anyProjectId}/${WorkspacePage.anyFileId}?page-id=${WorkspacePage.anyPageId}`, + ); + + this.#ws = await this.waitForNotificationsWebSocket(); + await this.#ws.mockOpen(); + await this.#waitForWebSocketReadiness(); + } + + async #waitForWebSocketReadiness() { + // TODO: find a better event to settle whether the app is ready to receive notifications via ws + await expect(this.pageName).toHaveText("Page 1"); + } + + async sendPresenceMessage(fixture) { + await this.#ws.mockMessage(JSON.stringify(fixture)); + } + + async cleanUp() { + await this.#ws.mockClose(); + } + + async setupEmptyFile() { + await this.mockRPC("get-profile", "logged-in-user/get-profile-logged-in.json"); + await this.mockRPC("get-team-users?file-id=*", "logged-in-user/get-team-users-single-user.json"); + await this.mockRPC("get-comment-threads?file-id=*", "workspace/get-comment-threads-empty.json"); + await this.mockRPC("get-project?id=*", "workspace/get-project-default.json"); + await this.mockRPC("get-team?id=*", "workspace/get-team-default.json"); + await this.mockRPC( + "get-profiles-for-file-comments?file-id=*", + "workspace/get-profile-for-file-comments.json", + ); + await this.mockRPC(/get\-file\?/, "workspace/get-file-blank.json"); + await this.mockRPC( + "get-file-object-thumbnails?file-id=*", + "workspace/get-file-object-thumbnails-blank.json", + ); + await this.mockRPC("get-font-variants?team-id=*", "workspace/get-font-variants-empty.json"); + await this.mockRPC("get-file-fragment?file-id=*", "workspace/get-file-fragment-blank.json"); + await this.mockRPC("get-file-libraries?file-id=*", "workspace/get-file-libraries-empty.json"); + } +} diff --git a/frontend/playwright/ui/specs/workspace.spec.js b/frontend/playwright/ui/specs/workspace.spec.js index 57a60e979..663e76b02 100644 --- a/frontend/playwright/ui/specs/workspace.spec.js +++ b/frontend/playwright/ui/specs/workspace.spec.js @@ -1,46 +1,26 @@ import { test, expect } from "@playwright/test"; -import { BaseWebSocketPage } from "../pages/BaseWebSocketPage"; -import { MockWebSocketHelper } from "../../helpers/MockWebSocketHelper"; +import { WorkspacePage } from "../pages/WorkspacePage"; 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) => { - page.mockRPC("get-profile", "logged-in-user/get-profile-logged-in.json"); - page.mockRPC("get-team-users?file-id=*", "logged-in-user/get-team-users-single-user.json"); - page.mockRPC("get-comment-threads?file-id=*", "workspace/get-comment-threads-empty.json"); - page.mockRPC("get-project?id=*", "workspace/get-project-default.json"); - page.mockRPC("get-team?id=*", "workspace/get-team-default.json"); - page.mockRPC(/get\-file\?/, "workspace/get-file-blank.json"); - page.mockRPC("get-file-object-thumbnails?file-id=*", "workspace/get-file-object-thumbnails-blank.json"); - page.mockRPC("get-profiles-for-file-comments?file-id=*", "workspace/get-profile-for-file-comments.json"); - page.mockRPC("get-font-variants?team-id=*", "workspace/get-font-variants-empty.json"); - page.mockRPC("get-file-fragment?file-id=*", "workspace/get-file-fragment-blank.json"); - page.mockRPC("get-file-libraries?file-id=*", "workspace/get-file-libraries-empty.json"); -}; - test.beforeEach(async ({ page }) => { - await MockWebSocketHelper.init(page); + await WorkspacePage.init(page); }); -test.skip("User loads worskpace with empty file", async ({ page }) => { - await setupWorkspaceUser(page); +test("User loads worskpace with empty file", async ({ page }) => { + const workspacePage = new WorkspacePage(page); + await workspacePage.setupEmptyFile(page); - await page.goto(`/#/workspace/${anyProjectId}/${anyFileId}?page-id=${anyPageId}`); + await workspacePage.goToWorkspace(); - await expect(page.getByTestId("page-name")).toHaveText("Page 1"); + await expect(workspacePage.pageName).toHaveText("Page 1"); }); -test.skip("User receives notifications updates in the workspace", async ({ page }) => { - await setupWorkspaceUser(page); - await page.goto(`/#/workspace/${anyProjectId}/${anyFileId}?page-id=${anyPageId}`); +test("User receives presence notifications updates in the workspace", async ({ page }) => { + const workspacePage = new WorkspacePage(page); + await workspacePage.setupEmptyFile(); + + await workspacePage.goToWorkspace(); + await workspacePage.sendPresenceMessage(presenceFixture); - 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(); }); From 00430d63eb596457eb384df3fe1de473fb641046 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Thu, 9 May 2024 12:57:51 +0200 Subject: [PATCH 3/3] :sparkles: Add test for drawing a shape in the workspace --- .../data/workspace/update-file-create-rect.json | 9 +++++++++ frontend/playwright/ui/pages/WorkspacePage.js | 12 +++++++++++- frontend/playwright/ui/specs/workspace.spec.js | 14 ++++++++++++++ frontend/src/app/main/ui/workspace/viewport.cljs | 2 +- 4 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 frontend/playwright/data/workspace/update-file-create-rect.json diff --git a/frontend/playwright/data/workspace/update-file-create-rect.json b/frontend/playwright/data/workspace/update-file-create-rect.json new file mode 100644 index 000000000..671fef98f --- /dev/null +++ b/frontend/playwright/data/workspace/update-file-create-rect.json @@ -0,0 +1,9 @@ +[ + { + "~:id": "~u088df3d4-d383-80f6-8004-527e50ea4f1f", + "~:revn": 21, + "~:file-id": "~uc7ce0794-0992-8105-8004-38f280443849", + "~:session-id": "~u1dc6d4fa-7bd3-803a-8004-527dd9df2c62", + "~:changes": [] + } +] diff --git a/frontend/playwright/ui/pages/WorkspacePage.js b/frontend/playwright/ui/pages/WorkspacePage.js index f89c57a7a..ce9b78dab 100644 --- a/frontend/playwright/ui/pages/WorkspacePage.js +++ b/frontend/playwright/ui/pages/WorkspacePage.js @@ -39,9 +39,11 @@ export class WorkspacePage extends BaseWebSocketPage { constructor(page) { super(page); - // TODO: add locators this.pageName = page.getByTestId("page-name"); this.presentUserListItems = page.getByTestId("active-users-list").getByAltText("Princesa Leia"); + this.viewport = page.getByTestId("viewport"); + this.rootShape = page.locator(`[id="shape-00000000-0000-0000-0000-000000000000"]`); + this.rectShapeButton = page.getByRole("button", { name: "Rectangle (R)" }); } async goToWorkspace() { @@ -86,4 +88,12 @@ export class WorkspacePage extends BaseWebSocketPage { await this.mockRPC("get-file-fragment?file-id=*", "workspace/get-file-fragment-blank.json"); await this.mockRPC("get-file-libraries?file-id=*", "workspace/get-file-libraries-empty.json"); } + + async clickWithDragViewportAt(x, y, width, height) { + await this.page.waitForTimeout(100); + await this.viewport.hover({ position: { x, y } }); + await this.page.mouse.down(); + await this.viewport.hover({ position: { x: x + width, y: y + height } }); + await this.page.mouse.up(); + } } diff --git a/frontend/playwright/ui/specs/workspace.spec.js b/frontend/playwright/ui/specs/workspace.spec.js index 663e76b02..832617911 100644 --- a/frontend/playwright/ui/specs/workspace.spec.js +++ b/frontend/playwright/ui/specs/workspace.spec.js @@ -24,3 +24,17 @@ test("User receives presence notifications updates in the workspace", async ({ p await expect(page.getByTestId("active-users-list").getByAltText("Princesa Leia")).toHaveCount(2); }); + +test("User draws a rect", async ({ page }) => { + const workspacePage = new WorkspacePage(page); + await workspacePage.setupEmptyFile(); + await workspacePage.mockRPC("update-file?id=*", "workspace/update-file-create-rect.json"); + + await workspacePage.goToWorkspace(); + await workspacePage.rectShapeButton.click(); + await workspacePage.clickWithDragViewportAt(128, 128, 200, 100); + + const shape = await workspacePage.rootShape.locator("rect"); + expect(shape).toHaveAttribute("width", "200"); + expect(shape).toHaveAttribute("height", "100"); +}); diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index d20d35e3c..d2697e018 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -276,7 +276,7 @@ (hooks/setup-shortcuts node-editing? drawing-path? text-editing? grid-editing?) (hooks/setup-active-frames base-objects hover-ids selected active-frames zoom transform vbox) - [:div.viewport {:style #js {"--zoom" zoom}} + [:div.viewport {:style #js {"--zoom" zoom} :data-testid "viewport"} [:& top-bar/top-bar {:layout layout}] [:div.viewport-overlays ;; The behaviour inside a foreign object is a bit different that in plain HTML so we wrap