diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 114849bb8b..e2a5d2bb41 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -167,10 +167,6 @@ jobs: - name: Running E2E tests run: yarn test:e2e - env: - PRISMIC_URL: https://wroom.io - WROOM_EMAIL: ${{ secrets.EMAIL }} - WROOM_PASSWORD: ${{ secrets.PASSWORD }} - name: Upload HTML report uses: actions/upload-artifact@v3 diff --git a/cypress/e2e/updates/changelog.cy.js b/cypress/e2e/updates/changelog.cy.js deleted file mode 100644 index f8b95cd3bb..0000000000 --- a/cypress/e2e/updates/changelog.cy.js +++ /dev/null @@ -1,45 +0,0 @@ -// TODO: Change this to an integration test. -describe.skip("changelog.warningBreakingChanges", () => { - beforeEach(() => { - cy.setSliceMachineUserContext({ - updatesViewed: { - latest: "1000.0.0", - latestNonBreaking: "1.2.3", - }, - }); - }); - - function mockChangelogCall(releaseNote) { - cy.intercept("GET", "/api/changelog", { - statusCode: 200, - body: { - currentVersion: "1000.0.0", - updateAvailable: true, - latestNonBreakingVersion: "1.2.3", - versions: [ - { - versionNumber: "1000.0.0", - status: "PATCH", - releaseNote, - }, - ], - }, - }); - } - - it("shows warning if the selected release note has a breaking changes title.", () => { - mockChangelogCall( - "### Breaking Changes\n -this changes is breaking your slice machine", - ); - cy.visit("/changelog"); - cy.waitUntil(() => cy.contains("All versions")); - cy.get("[data-testid=breaking-changes-warning]").should("exist"); - }); - - it("should not display the warning if the selected release note does not have a breaking changes title.", () => { - mockChangelogCall("This release does not include Breaking Changes"); - cy.visit("/changelog"); - cy.waitUntil(() => cy.contains("All versions")); - cy.get("[data-testid=breaking-changes-warning]").should("not.exist"); - }); -}); diff --git a/cypress/e2e/updates/sidebar.cy.js b/cypress/e2e/updates/sidebar.cy.js deleted file mode 100644 index 146b1d9671..0000000000 --- a/cypress/e2e/updates/sidebar.cy.js +++ /dev/null @@ -1,72 +0,0 @@ -// TODO: Change this to an integration test. -describe.skip("update notification", () => { - function mockChangelogCall(releaseNote) { - cy.intercept("GET", "/api/changelog", { - statusCode: 200, - body: { - currentVersion: "0.5.0", - updateAvailable: true, - latestNonBreakingVersion: "1.2.3", - versions: [ - { - versionNumber: "1000.0.0", - status: "PATCH", - releaseNote: null, - }, - ], - }, - }); - } - - it("updates available and user has not seen the notification", () => { - cy.setSliceMachineUserContext({}); - mockChangelogCall(); - - cy.visit("/"); - cy.get("[data-testid=the-red-dot]").should("exist"); - cy.contains("Learn more").click(); - cy.location("pathname", { timeout: 1000 }).should("eq", "/changelog"); - - cy.visit("/"); - cy.contains("Learn more").should("exist"); - cy.get("[data-testid=the-red-dot]").should("not.exist"); - - cy.getLocalStorage("persist:root").then((str) => { - const obj = JSON.parse(str); - const userContext = JSON.parse(obj.userContext); - - expect(userContext.updatesViewed).to.deep.equal({ - latest: "1000.0.0", - latestNonBreaking: "1.2.3", - }); - }); - }); - - it("updates available and user has seen the notification", () => { - cy.setSliceMachineUserContext({ - updatesViewed: { - latest: "1000.0.0", - latestNonBreaking: "1.2.3", - }, - }); - mockChangelogCall(); - - cy.visit("/"); - cy.contains("Learn more", { timeout: 60000 }).should("exist"); - cy.get("[data-testid=the-red-dot]").should("not.exist"); - }); - - it("user has seen the updates but an even newer on is available", () => { - cy.setSliceMachineUserContext({ - updatesViewed: { - latest: "999.0.0", - latestNonBreaking: "1.2.3", - }, - }); - mockChangelogCall(); - - cy.visit("/"); - cy.contains("Learn more").should("exist"); - cy.get("[data-testid=the-red-dot]").should("exist"); - }); -}); diff --git a/cypress/e2e/updates/simulator-tooltip.cy.js b/cypress/e2e/updates/simulator-tooltip.cy.js deleted file mode 100644 index e564b48f8a..0000000000 --- a/cypress/e2e/updates/simulator-tooltip.cy.js +++ /dev/null @@ -1,74 +0,0 @@ -/** This test needs to run AFTER create_slice. const values below are copied from there. */ -describe("simulator tooltip", () => { - const lib = ".--slices"; - const sliceName = "DuplicateSlices"; - const sliceId = "DuplicateSlices"; - - beforeEach("Cleanup local data", () => { - cy.clearProject(); - }); - - it("should display the tooltip when 'userContext.hasSeenSimulatorToolTip' is falsy and set to true when user clicks the close button", () => { - cy.setSliceMachineUserContext({ hasSeenSimulatorToolTip: false }); - - cy.createSlice(lib, sliceId, sliceName); - - cy.visit(`/slices/${lib}/${sliceName}/default`); - - // There is a 5 s timeout for displaying the tooltip. - cy.wait(6_000); - - cy.contains("Simulate your slices").should("exist"); - - cy.contains("Got It").click(); - - cy.contains("Simulate your slices").should("not.exist"); - - cy.getSliceMachineUserContext().should((data) => { - expect(data.hasSeenSimulatorToolTip).equal( - true, - "userContext.hasSeenSimulatorToolTip should set in local storage", - ); - }); - }); - - it("should not display when hasSeenSimulatorToolTip is truthy", () => { - cy.setSliceMachineUserContext({}); - - cy.createSlice(lib, sliceId, sliceName); - - cy.visit(`/slices/${lib}/${sliceName}/default`); - - // There is a 5 s timeout for displaying the tooltip. - cy.wait(6_000); - - cy.contains("Simulate your slices").should("not.exist"); - }); - - it("should close the tooltip when the user clicks the simulator button", () => { - cy.setSliceMachineUserContext({ hasSeenSimulatorToolTip: false }); - - cy.createSlice(lib, sliceId, sliceName); - - cy.visit(`/slices/${lib}/${sliceName}/default`); - - // There is a 5 s timeout for displaying the tooltip. - cy.wait(6_000); - - cy.contains("Simulate your slices").should("exist"); - - // Don't open the Simulator's window. - cy.window().then((win) => { - cy.stub(win, "open").as("Open"); - }); - - cy.get("[data-testid=simulator-open-button]").click(); - - cy.getSliceMachineUserContext().should((data) => { - expect(data.hasSeenSimulatorToolTip).equal( - true, - "userContext.hasSeenSimulatorToolTip should set in local storage", - ); - }); - }); -}); diff --git a/cypress/e2e/updates/video-tooltip.cy.js b/cypress/e2e/updates/video-tooltip.cy.js deleted file mode 100644 index 21aa5bd213..0000000000 --- a/cypress/e2e/updates/video-tooltip.cy.js +++ /dev/null @@ -1,75 +0,0 @@ -// TODO: DT-1435 - Handle tests when updating video item -describe.skip("video tooltip", () => { - it("should display the tooltip when 'userContext.hasSeenTutorialsToolTip' is falsy and set to true when user clicks the close button", () => { - cy.setSliceMachineUserContext({ hasSeenTutorialsToolTip: false }); - - cy.visit("/"); - - cy.get("[role=tooltip]", { timeout: 6_000 }).should("have.class", "show"); - - cy.get("[data-testid=video-tooltip-close-button]").click(); - - cy.get("[data-testid=video-tooltip]").should("not.exist"); - - cy.getSliceMachineUserContext().then((data) => { - expect(data.hasSeenTutorialsToolTip).equal( - true, - "userContext.hasSeenTutorialsToolTip should set in local storage", - ); - }); - }); - - it("should no display when hasSeenTutorialsToolTip is truthy", () => { - cy.setSliceMachineUserContext({}); - - cy.get("[role=tooltip]", { timeout: 6_000 }).should("not.exist"); - }); - - it("should close the tooltip when the user clicks the videos button", () => { - cy.setSliceMachineUserContext({ hasSeenTutorialsToolTip: false }); - - cy.visit("/"); - - cy.get("[role=tooltip]", { timeout: 6_000 }).should("have.class", "show"); - - cy.contains("Tutorial") - .should("have.attr", "target", "_blank") - .should( - "have.attr", - "href", - "https://youtube.com/playlist?list=PLUVZjQltoA3wnaQudcqQ3qdZNZ6hyfyhH", - ) - .click(); - - cy.getSliceMachineUserContext().should((data) => { - expect(data.hasSeenTutorialsToolTip).equal( - true, - "userContext.hasSeenTutorialsToolTip should set in local storage", - ); - }); - }); - - it("should disappear when the user hovers over the video toolbar", () => { - cy.setSliceMachineUserContext({ hasSeenTutorialsToolTip: false }); - - cy.visit("/"); - - cy.get("[role=tooltip]", { timeout: 6_000 }).should("have.class", "show"); - - cy.contains("Tutorial") - .trigger("mouseenter") - .trigger("mouseleave") - .trigger("mouseover") - .trigger("mousemove") - .trigger("mouseout"); - - cy.get("[data-testid=video-tooltip]").should("not.exist"); - - cy.getSliceMachineUserContext().should((data) => { - expect(data.hasSeenTutorialsToolTip).equal( - true, - "userContext.hasSeenTutorialsToolTip should set in local storage", - ); - }); - }); -}); diff --git a/e2e-projects/next/slicemachine.config.json b/e2e-projects/next/slicemachine.config.json index 5b8df5e0c3..8f9b3deb30 100644 --- a/e2e-projects/next/slicemachine.config.json +++ b/e2e-projects/next/slicemachine.config.json @@ -6,5 +6,5 @@ "slices/navigation" ], "adapter": "@slicemachine/adapter-next", - "localSliceSimulatorURL": "http://localhost:8000/slice-simulator" + "localSliceSimulatorURL": "http://localhost:3000/slice-simulator" } \ No newline at end of file diff --git a/package.json b/package.json index 4da7bc5bd4..a825344db5 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "dev:slice-machine-ui": "yarn workspace slice-machine-ui dev", "dev:adapter-next": "yarn workspace @slicemachine/adapter-next dev", "dev:adapter-sveltekit": "yarn workspace @slicemachine/adapter-sveltekit dev", - "dev:e2e-next": "cd ./e2e-projects/next && yarn dev --port 8000", + "dev:e2e-next": "cd ./e2e-projects/next && yarn dev", "clean-e2e-projects": "git checkout e2e-projects/ && git clean -f e2e-projects/", "postinstall": "husky install", "build": "yarn workspaces foreach --topological-dev --verbose run build && yarn run test", diff --git a/packages/slice-machine/components/Navigation/ChangesListItem.tsx b/packages/slice-machine/components/Navigation/ChangesListItem.tsx index 166f238b13..e285dc1292 100644 --- a/packages/slice-machine/components/Navigation/ChangesListItem.tsx +++ b/packages/slice-machine/components/Navigation/ChangesListItem.tsx @@ -58,7 +58,7 @@ export const ChangesListItem: FC = () => { When you click Save, your changes are saved locally. Then, you can push your models to Prismic from the Changes page. - Got It + Got it ); }; diff --git a/packages/slice-machine/lib/builders/SliceBuilder/SimulatorButton/index.tsx b/packages/slice-machine/lib/builders/SliceBuilder/SimulatorButton/index.tsx index bc965829d1..e1751dade2 100644 --- a/packages/slice-machine/lib/builders/SliceBuilder/SimulatorButton/index.tsx +++ b/packages/slice-machine/lib/builders/SliceBuilder/SimulatorButton/index.tsx @@ -65,7 +65,7 @@ const SimulatorOnboardingTooltip: React.FC< Minimize context-switching by previewing your Slice components in the simulator. - Got It + Got it ); }; diff --git a/packages/slice-machine/src/components/HoverCard/HoverCard.stories.tsx b/packages/slice-machine/src/components/HoverCard/HoverCard.stories.tsx index a2e5faa044..8d3d4ab4ad 100644 --- a/packages/slice-machine/src/components/HoverCard/HoverCard.stories.tsx +++ b/packages/slice-machine/src/components/HoverCard/HoverCard.stories.tsx @@ -37,7 +37,7 @@ export const Image = { Lorem ipsum dolor sit amet consectetur. Aenean purus aliquam vel eget vitae etiam - Got It + Got it ), }, @@ -62,7 +62,7 @@ export const Video = { Lorem ipsum dolor sit amet consectetur. Aenean purus aliquam vel eget vitae etiam - Got It + Got it ), }, diff --git a/playwright/.env.local.example b/playwright/.env.local.example deleted file mode 100644 index d7e8b75711..0000000000 --- a/playwright/.env.local.example +++ /dev/null @@ -1,5 +0,0 @@ -WROOM_EMAIL=email@example.com -WROOM_PASSWORD=my_password - -PRISMIC_EMAIL=email@example.com -PRISMIC_PASSWORD=my_password diff --git a/playwright/README.md b/playwright/README.md index 571b9e651f..7da3bdb5b0 100644 --- a/playwright/README.md +++ b/playwright/README.md @@ -10,13 +10,6 @@ _Install browsers and OS dependencies for Playwright._ yarn test:e2e:install ``` -- Create a `.env.local` file - -Copy-paste `playwright/.env.local.example` in `playwright/.env.local` and update `EMAIL` and `PASSWORD` values. - -Having both Wroom and Prismic values will help you run Slice Machine in dev or prod mode without having to take care of the correct email or password. -Wroom or Prismic values will be used depending on the Prismic URL. - - Install the VS Code extension (optional) Playwright Test extension was created specifically to accommodate the needs of e2e testing. [Install Playwright Test for VSCode by reading this page](https://playwright.dev/docs/getting-started-vscode). It will help you to debug a problem in tests if needed. @@ -79,13 +72,12 @@ npx playwright show-report name-of-my-extracted-playwright-report ### `test.run()` Use `test.run()` to create a test. `run` function take an optional object parameter `options` that let you configure how you want to run the test. -You can configure if you want a logged in test and also an onboarded test. -Default is not logged in and onboarded. +You can configure if you want an onboarded test. Default is onboarded. -Example for a logged in user not onboarded: +Example for user not onboarded: ```ts -test.run({ loggedIn: true, onboarded: false })("I can ...", +test.run({ onboarded: false })("I can ...", async ({ sliceBuilderPage, slicesListPage }) => { // Test content }); @@ -127,6 +119,31 @@ Warning: Only mock when it's necessary because the state of Slice Machine or the We want to ensure test can be launched on any state of Slice Machine and any state of repository. Mocking will help you do that. In theory, we want to avoid mocking while doing e2e tests. Smoke tests don't have any mocking but standalone tests can when it's necessary. It improves the DX and reduce the necessary setup that we can have for Smoke tests. +### Mocking with `mockLocalStorage` + +Use `mockLocalStorage` to dynamically update specific data in the Local Storage with ease. +It support the redux storage and also the new way to persist data in the Local Storage. + +Example (redux): +```ts +await mockLocalStorage({ + page, + reduxStorage: { + lastSyncChange: new Date().getTime(), + }, +}); +``` + +Example (new way): +```ts +await mockLocalStorage({ + page, + storage: { + isInAppGuideOpen: true, + }, +}); +``` + ## Best practices 1. Always use the "Page Object Model" for Locators @@ -212,7 +229,40 @@ test.run()("I can create a slice", async () => { }); ``` -6. Write your own best practice for the team here... +6. Always check that at least one locator is visible before checking if another locator is not visible + +Directly checking that a locator is not visible is not correct if the page is currently loading. The loading blank page will not contain your locator and it will always pass. + +Example (bad): + +```ts +test.run()( + "I cannot see the updates available warning", + async ({ pageTypesTablePage }) => { + await pageTypesTablePage.goto(); + await expect( + pageTypesTablePage.menu.updatesAvailableTitle, + ).not.toBeVisible(); + }, +); +``` + +Example (good): + +```ts +test.run()( + "I cannot see the updates available warning", + async ({ pageTypesTablePage }) => { + await pageTypesTablePage.goto(); + await expect(pageTypesTablePage.menu.appVersion).toBeVisible(); + await expect( + pageTypesTablePage.menu.updatesAvailableTitle, + ).not.toBeVisible(); + }, +); +``` + +7. Write your own best practice for the team here... ## Useful links diff --git a/playwright/fixtures/index.ts b/playwright/fixtures/index.ts index a98bcc9846..36ae4b5b52 100644 --- a/playwright/fixtures/index.ts +++ b/playwright/fixtures/index.ts @@ -1,8 +1,6 @@ -import * as dotenv from "dotenv"; import * as fs from "fs/promises"; import * as os from "os"; import * as path from "path"; -import { decode } from "@msgpack/msgpack"; import { test as baseTest, expect } from "@playwright/test"; import { PageTypesTablePage } from "../pages/PageTypesTablePage"; @@ -17,8 +15,6 @@ import { SliceMachinePage } from "../pages/SliceMachinePage"; import { generateRandomId } from "../utils/generateRandomId"; import config from "../playwright.config"; -dotenv.config({ path: `.env.local` }); - export type DefaultFixtures = { /** * Pages @@ -46,10 +42,8 @@ export type DefaultFixtures = { /** * Default test fixture */ -export const defaultTest = ( - options: { loggedIn?: boolean; onboarded?: boolean } = {}, -) => { - const { loggedIn = false, onboarded = true } = options; +export const defaultTest = (options: { onboarded?: boolean } = {}) => { + const { onboarded = true } = options; return baseTest.extend({ /** @@ -160,7 +154,7 @@ export const defaultTest = ( { name: "persist:root", value: JSON.stringify({ - userContext: { + userContext: JSON.stringify({ userReview: { onboarding: true, advancedRepository: true, @@ -174,7 +168,7 @@ export const defaultTest = ( hasSeenTutorialsToolTip: true, authStatus: "unknown", lastSyncChange: null, - }, + }), }), }, { @@ -212,99 +206,6 @@ export const defaultTest = ( // Ignore since it means the user is already logged out } - // Login user if needed - if (loggedIn) { - // In CI we define a PRISMIC_URL env variable to fasten the tests - let prismicUrl = process.env["PRISMIC_URL"]; - - // In local we get the Prismic URL from the browser, it helps to avoid - // switching manually between Wroom and Prismic - if (!prismicUrl) { - await page.route( - "*/**/_manager", - async (route) => { - const postDataBuffer = route.request().postDataBuffer() as Buffer; - const postData = decode(postDataBuffer) as Record< - "procedurePath", - unknown[] - >; - - if (postData.procedurePath[0] === "getState") { - const response = await route.fetch(); - const existingBody = await response.body(); - const existingData = ( - decode(existingBody) as Record<"data", unknown> - ).data as Record< - "env", - { - endpoints: { PrismicWroom: string }; - } - >; - - // Get Prismic URL from the response of the getState call - prismicUrl = existingData.env.endpoints.PrismicWroom; - - await route.continue(); - } - }, - // Ensure only the first getState call is intercepted - { - times: 1, - }, - ); - - // Visit the page to trigger the getState call and wait for it - await page.goto("/"); - await page.waitForResponse("*/**/_manager"); - } - - const activeEnv = prismicUrl?.includes("wroom") ? "WROOM" : "PRISMIC"; - const email = process.env[`${activeEnv}_EMAIL`]; - const password = process.env[`${activeEnv}_PASSWORD`]; - - if (!prismicUrl) { - console.warn("Could not find Prismic URL."); - } else if (!email || !password) { - console.warn( - `Missing EMAIL or PASSWORD environment variables for ${activeEnv} environment.`, - ); - } else { - // Do the authentication call - const res = await fetch( - new URL("./authentication/signin", prismicUrl).toString(), - { - method: "post", - body: JSON.stringify({ - email, - password, - }), - headers: { - "Content-Type": "application/json", - }, - }, - ); - - if (!res.headers.has("Set-Cookie")) { - // If the authentication fails, log the error - const reason = await res.text(); - console.error( - "Could not authenticate to prismic. Please check the credentials.", - reason, - ); - } else { - // If the authentication succeeded, save the cookies to persist it - await fs.writeFile( - path.join(os.homedir(), ".prismic"), - JSON.stringify({ - base: new URL(prismicUrl).toString(), - cookies: - res.headers.get("Set-Cookie")?.split(", ").join("; ") ?? "", - }), - ); - } - } - } - // Propagate the modified page to the test await use(page); }, diff --git a/playwright/package.json b/playwright/package.json index 1687f5b04d..53d3fb1703 100644 --- a/playwright/package.json +++ b/playwright/package.json @@ -8,7 +8,7 @@ "lint": "eslint --max-warnings 0 --ext .js,.ts .", "test:e2e:install": "playwright install --with-deps chromium", "test:e2e:report": "playwright show-report", - "test:e2e": "playwright test", + "test:e2e": "DEBUG=pw:test playwright test", "types": "tsc --noEmit", "unit": "echo \"No unit tests for playwright\" && true", "depcheck": "depcheck --config=.depcheckrc", @@ -20,7 +20,6 @@ "@typescript-eslint/eslint-plugin": "5.55.0", "@typescript-eslint/parser": "5.55.0", "depcheck": "1.4.3", - "dotenv": "16.3.1", "eslint": "8.37.0", "eslint-config-prettier": "9.0.0", "typescript": "4.9.5" diff --git a/playwright/pages/ChangelogPage.ts b/playwright/pages/ChangelogPage.ts index 6874f7978b..0328ac60c3 100644 --- a/playwright/pages/ChangelogPage.ts +++ b/playwright/pages/ChangelogPage.ts @@ -1,9 +1,11 @@ -import { Locator, Page } from "@playwright/test"; +import { Locator, Page, expect } from "@playwright/test"; import { SliceMachinePage } from "./SliceMachinePage"; export class ChangelogPage extends SliceMachinePage { + readonly path: string; readonly breadcrumbLabel: Locator; + readonly breakingChangesWarning: Locator; constructor(page: Page) { super(page); @@ -16,7 +18,12 @@ export class ChangelogPage extends SliceMachinePage { /** * Static locators */ + this.path = "/changelog"; this.breadcrumbLabel = this.body.getByText("Changelog", { exact: true }); + this.breakingChangesWarning = this.body.getByText( + "This update includes breaking changes. To update correctly, follow the steps below.", + { exact: true }, + ); } /** @@ -27,10 +34,20 @@ export class ChangelogPage extends SliceMachinePage { /** * Actions */ - // Handle actions here + async goto() { + await this.page.goto(this.path); + } + + async selectVersion(version: string) { + await this.body.getByText(version, { exact: true }).click(); + } /** * Assertions */ - // Handle assertions here + async checkReleaseNotes(releaseNotes: string) { + await expect( + this.body.getByText(releaseNotes, { exact: true }), + ).toBeVisible(); + } } diff --git a/playwright/pages/SliceBuilderPage.ts b/playwright/pages/SliceBuilderPage.ts index a42d7f8e6f..62c5c9c705 100644 --- a/playwright/pages/SliceBuilderPage.ts +++ b/playwright/pages/SliceBuilderPage.ts @@ -12,6 +12,8 @@ export class SliceBuilderPage extends BuilderPage { readonly renameVariationDialog: RenameVariationDialog; readonly deleteVariationDialog: DeleteVariationDialog; readonly savedMessage: Locator; + readonly simulateTooltipTitle: Locator; + readonly simulateTooltipCloseButton: Locator; readonly variationCards: Locator; readonly addVariationButton: Locator; readonly staticZone: Locator; @@ -39,6 +41,8 @@ export class SliceBuilderPage extends BuilderPage { this.savedMessage = page.getByText("Slice saved successfully", { exact: false, }); + this.simulateTooltipTitle = page.getByText("Simulate your slices"); + this.simulateTooltipCloseButton = page.getByText("Got it"); // Variations this.variationCards = page.getByRole("link", { name: "slice card", diff --git a/playwright/pages/SliceMachinePage.ts b/playwright/pages/SliceMachinePage.ts index 396bd23d44..b4a691177b 100644 --- a/playwright/pages/SliceMachinePage.ts +++ b/playwright/pages/SliceMachinePage.ts @@ -1,11 +1,13 @@ import { Locator, Page } from "@playwright/test"; import { Menu } from "./components/Menu"; +import { ReviewDialog } from "./components/ReviewDialog"; import { InAppGuideDialog } from "./components/InAppGuideDialog"; export class SliceMachinePage { readonly page: Page; readonly menu: Menu; + readonly reviewDialog: ReviewDialog; readonly inAppGuideDialog: InAppGuideDialog; readonly body: Locator; readonly breadcrumb: Locator; @@ -16,6 +18,7 @@ export class SliceMachinePage { */ this.page = page; this.menu = new Menu(page); + this.reviewDialog = new ReviewDialog(page); this.inAppGuideDialog = new InAppGuideDialog(page); /** diff --git a/playwright/pages/components/Menu.ts b/playwright/pages/components/Menu.ts index c75450f153..f4eb50eb22 100644 --- a/playwright/pages/components/Menu.ts +++ b/playwright/pages/components/Menu.ts @@ -10,6 +10,10 @@ export class Menu { readonly tutorialLink: Locator; readonly changelogLink: Locator; readonly appVersion: Locator; + readonly updatesAvailableTitle: Locator; + readonly updatesAvailableButton: Locator; + readonly tutorialVideoTooltipTitle: Locator; + readonly tutorialVideoTooltipCloseButton: Locator; constructor(page: Page) { /** @@ -43,12 +47,22 @@ export class Menu { exact: false, }); this.appVersion = this.menu.getByTestId("slicemachine-version"); + this.updatesAvailableTitle = this.menu.getByText("Updates Available", { + exact: true, + }); + this.updatesAvailableButton = this.menu.getByText("Learn more", { + exact: true, + }); + this.tutorialVideoTooltipTitle = page.getByText("Need Help?"); + this.tutorialVideoTooltipCloseButton = page.getByText("Got it"); } /** * Dynamic locators */ - // Handle dynamic locators here + getAppVersion(appVersion: string) { + return this.appVersion.getByText(`v${appVersion}`, { exact: true }); + } /** * Actions diff --git a/playwright/pages/components/ReviewDialog.ts b/playwright/pages/components/ReviewDialog.ts new file mode 100644 index 0000000000..09e9ff0681 --- /dev/null +++ b/playwright/pages/components/ReviewDialog.ts @@ -0,0 +1,36 @@ +import { Page } from "@playwright/test"; + +import { Dialog } from "./Dialog"; + +export class ReviewDialog extends Dialog { + constructor(page: Page) { + super(page, { + title: "Share feedback", + }); + + /** + * Components + */ + // Handle components here + + /** + * Static locators + */ + // Handle static locators here + } + + /** + * Dynamic locators + */ + // Handle dynamic locators here + + /** + * Actions + */ + // Handle actions here + + /** + * Assertions + */ + // Handle assertions here +} diff --git a/playwright/pages/components/SelectExistingSlicesDialog.ts b/playwright/pages/components/SelectExistingSlicesDialog.ts new file mode 100644 index 0000000000..e439842425 --- /dev/null +++ b/playwright/pages/components/SelectExistingSlicesDialog.ts @@ -0,0 +1,49 @@ +import { expect, Locator, Page } from "@playwright/test"; + +import { Dialog } from "./Dialog"; + +export class SelectExistingSlicesDialog extends Dialog { + readonly sharedSliceCard: Locator; + readonly addedMessage: Locator; + + constructor(page: Page) { + super(page, { + title: `Select existing slices`, + submitName: "Add", + }); + + /** + * Static locators + */ + this.sharedSliceCard = this.dialog.getByTestId("shared-slice-card"); + this.addedMessage = page.getByText("Slice(s) added to slice zone", { + exact: true, + }); + } + + /** + * Dynamic locators + */ + // Handle dynamic locators here + + /** + * Actions + */ + async selectExistingSlices(names: string[]) { + await expect(this.title).toBeVisible(); + for (const name of names) { + await this.sharedSliceCard.getByText(name, { exact: true }).click(); + } + await this.submitButton.click(); + await this.checkCreatedMessage(); + await expect(this.title).not.toBeVisible(); + } + + /** + * Assertions + */ + async checkCreatedMessage() { + await expect(this.addedMessage).toBeVisible(); + await expect(this.addedMessage).not.toBeVisible(); + } +} diff --git a/playwright/pages/shared/TypeBuilderPage.ts b/playwright/pages/shared/TypeBuilderPage.ts index 8c0fd08a8a..686ab07040 100644 --- a/playwright/pages/shared/TypeBuilderPage.ts +++ b/playwright/pages/shared/TypeBuilderPage.ts @@ -3,6 +3,7 @@ import { expect, Locator, Page } from "@playwright/test"; import { CreateTypeDialog } from "../components/CreateTypeDialog"; import { RenameTypeDialog } from "../components/RenameTypeDialog"; import { UseTemplateSlicesDialog } from "../components/UseTemplateSlicesDialog"; +import { SelectExistingSlicesDialog } from "../components/SelectExistingSlicesDialog"; import { CustomTypesTablePage } from "../CustomTypesTablePage"; import { BuilderPage } from "./BuilderPage"; import { PageTypesTablePage } from "../PageTypesTablePage"; @@ -11,6 +12,7 @@ export class TypeBuilderPage extends BuilderPage { readonly createTypeDialog: CreateTypeDialog; readonly renameTypeDialog: RenameTypeDialog; readonly useTemplateSlicesDialog: UseTemplateSlicesDialog; + readonly selectExistingSlicesDialog: SelectExistingSlicesDialog; readonly customTypeTablePage: CustomTypesTablePage; readonly pageTypeTablePage: PageTypesTablePage; readonly format: "page" | "custom"; @@ -24,6 +26,7 @@ export class TypeBuilderPage extends BuilderPage { readonly sliceZoneBlankSlate: Locator; readonly sliceZoneBlankSlateTitle: Locator; readonly sliceZoneUseTemplateAction: Locator; + readonly sliceZoneSelectExistingAction: Locator; readonly sliceZoneSharedSliceCard: Locator; constructor( @@ -41,6 +44,7 @@ export class TypeBuilderPage extends BuilderPage { this.createTypeDialog = new CreateTypeDialog(page, format); this.renameTypeDialog = new RenameTypeDialog(page, format); this.useTemplateSlicesDialog = new UseTemplateSlicesDialog(page); + this.selectExistingSlicesDialog = new SelectExistingSlicesDialog(page); this.customTypeTablePage = new CustomTypesTablePage(page); this.pageTypeTablePage = new PageTypesTablePage(page); @@ -77,6 +81,9 @@ export class TypeBuilderPage extends BuilderPage { this.sliceZoneUseTemplateAction = page.getByText("Use template", { exact: true, }); + this.sliceZoneSelectExistingAction = page.getByText("Select existing", { + exact: true, + }); this.sliceZoneSharedSliceCard = page.getByTestId("shared-slice-card"); } diff --git a/playwright/playwright.config.ts b/playwright/playwright.config.ts index f9d6f0037e..5ab62b9393 100644 --- a/playwright/playwright.config.ts +++ b/playwright/playwright.config.ts @@ -9,9 +9,6 @@ const config = { timeout: 30_000, }, - // 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, @@ -80,10 +77,6 @@ const config = { timeout: 120_000, }, ], - - // Don't run tests in parallel due to the nature of - // Slice Machine modifying file in the file system. - workers: 1, } satisfies PlaywrightTestConfig; export default config; diff --git a/playwright/tests/changelog/changelog.spec.ts b/playwright/tests/changelog/changelog.spec.ts index 989fbc7c80..c152705fcf 100644 --- a/playwright/tests/changelog/changelog.spec.ts +++ b/playwright/tests/changelog/changelog.spec.ts @@ -1,5 +1,76 @@ +import { expect } from "@playwright/test"; + import { test } from "../../fixtures"; +import { mockManagerProcedures } from "../../utils"; + +test.describe("Changelog", () => { + test.run()( + "I can see a warning if the selected release note has a breaking changes", + async ({ changelogPage }) => { + const releaseNotes = + "Breaking Changes - This changes is breaking your slice machine"; + await mockManagerProcedures({ + page: changelogPage.page, + procedures: [ + { + path: "versions.getAllStableSliceMachineVersionsWithKind", + data: () => [ + { + version: "2.0.0", + kind: "MAJOR", + }, + ], + execute: false, + }, + { + path: "versions.getSliceMachineReleaseNotesForVersion", + data: () => `# ${releaseNotes}`, + execute: false, + }, + ], + }); + + await changelogPage.goto(); + + await changelogPage.checkReleaseNotes(releaseNotes); + await expect(changelogPage.breakingChangesWarning).toBeVisible(); + }, + ); + + test.run()( + "I cannot see a warning if the selected release note don't have a breaking changes", + async ({ changelogPage }) => { + const releaseNotes = "This changes is not breaking your slice machine"; + await mockManagerProcedures({ + page: changelogPage.page, + procedures: [ + { + path: "versions.getAllStableSliceMachineVersionsWithKind", + data: () => [ + { + version: "2.0.0", + kind: "MAJOR", + }, + { + version: "1.0.42", + kind: "PATCH", + }, + ], + execute: false, + }, + { + path: "versions.getSliceMachineReleaseNotesForVersion", + data: () => releaseNotes, + execute: false, + }, + ], + }); + + await changelogPage.goto(); + await changelogPage.selectVersion("1.0.42"); -test.describe.skip("Changelog", () => { - // TODO: Add tests + await changelogPage.checkReleaseNotes(releaseNotes); + await expect(changelogPage.breakingChangesWarning).not.toBeVisible(); + }, + ); }); diff --git a/playwright/tests/changes/changes.spec.ts b/playwright/tests/changes/changes.spec.ts index 25a104c21a..a37f0d5707 100644 --- a/playwright/tests/changes/changes.spec.ts +++ b/playwright/tests/changes/changes.spec.ts @@ -5,9 +5,22 @@ import { mockManagerProcedures } from "../../utils"; import { emptyLibraries, simpleCustomType } from "../../mocks"; test.describe("Changes", () => { - test.run({ loggedIn: true })( + test.run()( "I cannot see the login screen when logged in", async ({ changesPage }) => { + await mockManagerProcedures({ + page: changesPage.page, + procedures: [ + { + path: "getState", + data: (data) => ({ + ...data, + clientError: undefined, + }), + }, + ], + }); + await changesPage.goto(); await expect(changesPage.loginButton).not.toBeVisible(); await expect(changesPage.notLoggedInTitle).not.toBeVisible(); @@ -18,13 +31,28 @@ test.describe("Changes", () => { test.run()( "I can see the login screen when logged out", async ({ changesPage }) => { + await mockManagerProcedures({ + page: changesPage.page, + procedures: [ + { + path: "getState", + data: (data) => ({ + ...data, + clientError: { + status: 401, + }, + }), + }, + ], + }); + await changesPage.goto(); await expect(changesPage.loginButton).toBeVisible(); await expect(changesPage.notLoggedInTitle).toBeVisible(); }, ); - test.run({ loggedIn: true })( + test.run()( "I can see the unauthorized screen when not authorized", async ({ changesPage }) => { await mockManagerProcedures({ @@ -48,7 +76,7 @@ test.describe("Changes", () => { }, ); - test.run({ loggedIn: true })( + test.run()( "I can see the empty state when I don't have any changes to push", async ({ changesPage }) => { await mockManagerProcedures({ @@ -62,6 +90,7 @@ test.describe("Changes", () => { customTypes: [], remoteCustomTypes: [], remoteSlices: [], + clientError: undefined, }), }, ], @@ -73,7 +102,7 @@ test.describe("Changes", () => { }, ); - test.run({ loggedIn: true })( + test.run()( "I can see the changes I have to push", async ({ changesPage }) => { await mockManagerProcedures({ @@ -87,6 +116,7 @@ test.describe("Changes", () => { customTypes: [simpleCustomType], remoteCustomTypes: [], remoteSlices: [], + clientError: undefined, }), }, ], @@ -103,32 +133,30 @@ test.describe("Changes", () => { }, ); - test.run({ loggedIn: true })( - "I can push the changes I have", - async ({ changesPage }) => { - await mockManagerProcedures({ - page: changesPage.page, - procedures: [ - { - path: "getState", - data: (data) => ({ - ...data, - libraries: emptyLibraries, - customTypes: [simpleCustomType], - remoteCustomTypes: [], - remoteSlices: [], - }), - }, - { - path: "prismicRepository.pushChanges", - execute: false, - }, - ], - }); + test.run()("I can push the changes I have", async ({ changesPage }) => { + await mockManagerProcedures({ + page: changesPage.page, + procedures: [ + { + path: "getState", + data: (data) => ({ + ...data, + libraries: emptyLibraries, + customTypes: [simpleCustomType], + remoteCustomTypes: [], + remoteSlices: [], + clientError: undefined, + }), + }, + { + path: "prismicRepository.pushChanges", + execute: false, + }, + ], + }); - await changesPage.goto(); - await expect(changesPage.loginButton).not.toBeVisible(); - await changesPage.pushChanges(); - }, - ); + await changesPage.goto(); + await expect(changesPage.loginButton).not.toBeVisible(); + await changesPage.pushChanges(); + }); }); diff --git a/playwright/tests/common/navigation.spec.ts b/playwright/tests/common/navigation.spec.ts deleted file mode 100644 index e4109baa4e..0000000000 --- a/playwright/tests/common/navigation.spec.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { expect } from "@playwright/test"; - -import { test } from "../../fixtures"; - -test.describe("Navigation", () => { - test.run()( - "I can navigate through all menu entries", - async ({ - sliceMachinePage, - pageTypesTablePage, - customTypesTablePage, - slicesListPage, - changesPage, - changelogPage, - }) => { - await sliceMachinePage.gotoDefaultPage(); - - await pageTypesTablePage.menu.pageTypesLink.click(); - await expect(pageTypesTablePage.breadcrumbLabel).toBeVisible(); - expect(await sliceMachinePage.page.title()).toContain( - "Page types - Slice Machine", - ); - - await customTypesTablePage.menu.customTypesLink.click(); - await expect(customTypesTablePage.breadcrumbLabel).toBeVisible(); - expect(await sliceMachinePage.page.title()).toContain( - "Custom types - Slice Machine", - ); - - await slicesListPage.menu.slicesLink.click(); - await expect(slicesListPage.breadcrumbLabel).toBeVisible(); - expect(await sliceMachinePage.page.title()).toContain( - "Slices - Slice Machine", - ); - - await changesPage.menu.changesLink.click(); - await expect(changesPage.breadcrumbLabel).toBeVisible(); - expect(await sliceMachinePage.page.title()).toContain( - "Changes - Slice Machine", - ); - - await changelogPage.menu.changelogLink.click(); - await expect(changelogPage.breadcrumbLabel).toBeVisible(); - expect(await sliceMachinePage.page.title()).toContain( - "Changelog - Slice Machine", - ); - }, - ); - - // Unskip when we fix the Changelog fetching problem - DT-1794 - test - .run() - .skip( - "I access the changelog from Slice Machine version", - async ({ pageTypesTablePage, changelogPage }) => { - await pageTypesTablePage.goto(); - await expect(pageTypesTablePage.menu.appVersion).toBeVisible(); - await pageTypesTablePage.menu.appVersion.click(); - - await expect(changelogPage.breadcrumbLabel).toBeVisible(); - }, - ); -}); diff --git a/playwright/tests/common/sideNav.spec.ts b/playwright/tests/common/sideNav.spec.ts new file mode 100644 index 0000000000..9e88814880 --- /dev/null +++ b/playwright/tests/common/sideNav.spec.ts @@ -0,0 +1,168 @@ +import { expect } from "@playwright/test"; + +import { test } from "../../fixtures"; +import { mockManagerProcedures } from "../../utils/mockManagerProcedures"; +import { mockLocalStorage } from "../../utils/mockLocalStorage"; + +test.describe("Side nav", () => { + test.run()( + "I can navigate through all menu entries", + async ({ + sliceMachinePage, + pageTypesTablePage, + customTypesTablePage, + slicesListPage, + changesPage, + changelogPage, + }) => { + await sliceMachinePage.gotoDefaultPage(); + + await pageTypesTablePage.menu.pageTypesLink.click(); + await expect(pageTypesTablePage.breadcrumbLabel).toBeVisible(); + expect(await sliceMachinePage.page.title()).toContain( + "Page types - Slice Machine", + ); + + await customTypesTablePage.menu.customTypesLink.click(); + await expect(customTypesTablePage.breadcrumbLabel).toBeVisible(); + expect(await sliceMachinePage.page.title()).toContain( + "Custom types - Slice Machine", + ); + + await slicesListPage.menu.slicesLink.click(); + await expect(slicesListPage.breadcrumbLabel).toBeVisible(); + expect(await sliceMachinePage.page.title()).toContain( + "Slices - Slice Machine", + ); + + await changesPage.menu.changesLink.click(); + await expect(changesPage.breadcrumbLabel).toBeVisible(); + expect(await sliceMachinePage.page.title()).toContain( + "Changes - Slice Machine", + ); + + await changelogPage.menu.changelogLink.click(); + await expect(changelogPage.breadcrumbLabel).toBeVisible(); + expect(await sliceMachinePage.page.title()).toContain( + "Changelog - Slice Machine", + ); + }, + ); + + test.run()( + "I access the changelog from Slice Machine version", + async ({ pageTypesTablePage, changelogPage }) => { + await mockManagerProcedures({ + page: pageTypesTablePage.page, + procedures: [ + { + path: "versions.getRunningSliceMachineVersion", + data: () => "1.0.42", + execute: false, + }, + ], + }); + + await pageTypesTablePage.goto(); + await pageTypesTablePage.menu.getAppVersion("1.0.42").click(); + + await expect(changelogPage.breadcrumbLabel).toBeVisible(); + }, + ); + + test.run()( + "I can see the updates available warning and access changelog from it", + async ({ pageTypesTablePage, changelogPage }) => { + await mockManagerProcedures({ + page: pageTypesTablePage.page, + procedures: [ + { + path: "versions.checkIsSliceMachineUpdateAvailable", + data: () => true, + execute: false, + }, + ], + }); + + await pageTypesTablePage.goto(); + await expect(pageTypesTablePage.menu.updatesAvailableTitle).toBeVisible(); + await pageTypesTablePage.menu.updatesAvailableButton.click(); + + await expect(changelogPage.breadcrumbLabel).toBeVisible(); + }, + ); + + test.run()( + "I cannot see the updates available warning", + async ({ pageTypesTablePage }) => { + await mockManagerProcedures({ + page: pageTypesTablePage.page, + procedures: [ + { + path: "versions.checkIsAdapterUpdateAvailable", + data: () => false, + execute: false, + }, + { + path: "versions.checkIsSliceMachineUpdateAvailable", + data: () => false, + execute: false, + }, + ], + }); + + await pageTypesTablePage.goto(); + await expect(pageTypesTablePage.menu.appVersion).toBeVisible(); + await expect( + pageTypesTablePage.menu.updatesAvailableTitle, + ).not.toBeVisible(); + }, + ); + + test.run({ onboarded: false })( + "I can close the tutorial video tooltip and it stays close", + async ({ + pageTypesBuilderPage, + reusablePageType, + slice, + sliceMachinePage, + pageTypesTablePage, + }) => { + // We create a page type with a slice that is a requirement for the review dialog + await pageTypesBuilderPage.goto(reusablePageType.name); + await pageTypesBuilderPage.sliceZoneSelectExistingAction.click(); + await pageTypesBuilderPage.selectExistingSlicesDialog.selectExistingSlices( + [slice.name], + ); + + // We simulate a push more than an hour ago + await mockLocalStorage({ + page: pageTypesBuilderPage.page, + reduxStorage: { + lastSyncChange: new Date(new Date().getTime() - 3600000).getTime(), + }, + }); + await sliceMachinePage.page.reload(); + + // We close the in app guide and review dialogs that are requirements for the tutorial tooltip display + await sliceMachinePage.inAppGuideDialog.closeButton.click(); + await sliceMachinePage.reviewDialog.closeButton.click(); + + // Then tutorial tooltip open after the review dialog + await expect( + sliceMachinePage.menu.tutorialVideoTooltipTitle, + ).toBeVisible(); + await sliceMachinePage.menu.tutorialVideoTooltipCloseButton.click(); + await expect( + sliceMachinePage.menu.tutorialVideoTooltipTitle, + ).not.toBeVisible(); + + await sliceMachinePage.page.reload(); + await expect(pageTypesTablePage.menu.pageTypesLink).toBeVisible(); + + await expect( + sliceMachinePage.menu.tutorialVideoTooltipTitle, + ).not.toBeVisible(); + }, + ); +}); diff --git a/playwright/tests/slices/sliceBuilder.spec.ts b/playwright/tests/slices/sliceBuilder.spec.ts index dce815a946..3b3c2bfa1d 100644 --- a/playwright/tests/slices/sliceBuilder.spec.ts +++ b/playwright/tests/slices/sliceBuilder.spec.ts @@ -85,4 +85,23 @@ test.describe("Slice builder", () => { await expect(sliceBuilderPage.staticZoneListItem).toHaveCount(1); }, ); + + test.run({ onboarded: false })( + "I can close the simulator tooltip and it stays close", + async ({ slice, sliceBuilderPage }) => { + await sliceBuilderPage.goto(slice.name); + + // Simulator tooltip should open automatically + await expect(sliceBuilderPage.simulateTooltipTitle).toBeVisible(); + await sliceBuilderPage.simulateTooltipCloseButton.click(); + await expect(sliceBuilderPage.simulateTooltipTitle).not.toBeVisible(); + + await sliceBuilderPage.page.reload(); + await expect( + sliceBuilderPage.getBreadcrumbLabel(slice.name), + ).toBeVisible(); + + await expect(sliceBuilderPage.simulateTooltipTitle).not.toBeVisible(); + }, + ); }); diff --git a/playwright/utils/index.ts b/playwright/utils/index.ts index 0641616f40..709d513724 100644 --- a/playwright/utils/index.ts +++ b/playwright/utils/index.ts @@ -1,2 +1,3 @@ export { generateRandomId } from "./generateRandomId"; export { mockManagerProcedures } from "./mockManagerProcedures"; +export { mockLocalStorage } from "./mockLocalStorage"; diff --git a/playwright/utils/mockLocalStorage.ts b/playwright/utils/mockLocalStorage.ts new file mode 100644 index 0000000000..9318d8b0dd --- /dev/null +++ b/playwright/utils/mockLocalStorage.ts @@ -0,0 +1,43 @@ +import { Page } from "@playwright/test"; + +type MockReduxLocalStorageArgs = { + page: Page; + storage?: Record; + reduxStorage?: Record; +}; + +const SLICE_MACHINE_STORAGE_PREFIX = "slice-machine"; + +export async function mockLocalStorage(args: MockReduxLocalStorageArgs) { + const { page, storage = {}, reduxStorage = {} } = args; + + await page.evaluate( + ({ reduxStorage, storage }) => { + // Storage + Object.entries(storage).forEach(([itemKey, itemValue]) => { + localStorage.setItem( + `${SLICE_MACHINE_STORAGE_PREFIX}_${itemKey}`, + JSON.stringify(itemValue), + ); + }); + + // Redux storage + const existingReduxStorage = JSON.parse( + localStorage.getItem("persist:root") as string, + ) as { userContext: string }; + const userContext = JSON.parse( + existingReduxStorage.userContext, + ) as Record; + localStorage.setItem( + "persist:root", + JSON.stringify({ + userContext: JSON.stringify({ + ...userContext, + ...reduxStorage, + }), + }), + ); + }, + { reduxStorage, storage }, + ); +} diff --git a/playwright/utils/mockManagerProcedures.ts b/playwright/utils/mockManagerProcedures.ts index 79a40f8c4b..78a5d3b56f 100644 --- a/playwright/utils/mockManagerProcedures.ts +++ b/playwright/utils/mockManagerProcedures.ts @@ -19,7 +19,9 @@ type MockManagerProceduresArgs = { /** * Function that takes the existing data and returns the data to return. */ - data?: (data: Record) => Record; + data?: ( + data: Record, + ) => Record | Record[] | string | boolean; /** * Whether to execute the procedure or not. Defaults to true. diff --git a/yarn.lock b/yarn.lock index 4c3bb83b47..98b9480e37 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8540,7 +8540,6 @@ __metadata: "@typescript-eslint/eslint-plugin": 5.55.0 "@typescript-eslint/parser": 5.55.0 depcheck: 1.4.3 - dotenv: 16.3.1 eslint: 8.37.0 eslint-config-prettier: 9.0.0 typescript: 4.9.5