diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 114849bb8b..4434451e2b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -168,9 +168,7 @@ jobs: - name: Running E2E tests run: yarn test:e2e env: - PRISMIC_URL: https://wroom.io - WROOM_EMAIL: ${{ secrets.EMAIL }} - WROOM_PASSWORD: ${{ secrets.PASSWORD }} + DEBUG: pw:test - name: Upload HTML report uses: actions/upload-artifact@v3 diff --git a/cypress/e2e/slices/01-duplicates.cy.js b/cypress/e2e/slices/01-duplicates.cy.js deleted file mode 100644 index 39b105a1d9..0000000000 --- a/cypress/e2e/slices/01-duplicates.cy.js +++ /dev/null @@ -1,25 +0,0 @@ -const sliceName = "DuplicateSlices"; -const sliceId = "duplicate_slices"; -const lib = ".--slices"; - -describe("Duplicate Slices", () => { - beforeEach(() => { - cy.setSliceMachineUserContext({}); - cy.clearProject(); - }); - - it("A user can not create two slices with the same name", () => { - cy.createSlice(lib, sliceId, sliceName); - - // do it again - cy.visit(`/slices`); - cy.get("[data-cy=create-slice]").click(); - cy.get("[data-cy=create-slice-modal]").should("be.visible"); - - cy.get("input[data-cy=slice-name-input]").type(sliceName).blur(); - cy.get("[type=submit]").should("be.disabled"); - cy.get("[data-cy=slice-name-input-error]").contains( - "Slice name is already taken.", - ); - }); -}); 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/cypress/e2e/user-flows/scenario_slice_association.cy.js b/cypress/e2e/user-flows/scenario_slice_association.cy.js index db35185886..d1117cc88a 100644 --- a/cypress/e2e/user-flows/scenario_slice_association.cy.js +++ b/cypress/e2e/user-flows/scenario_slice_association.cy.js @@ -78,50 +78,4 @@ describe.skip("I am an existing SM user (Next) and I want to associate a Slice t cy.contains("Repeatable Rich Text Field"); cy.contains("Repeatable Key Text Field"); }); - - it("Push the newly created custom type and slice", () => { - changesPage.goTo().pushChanges().isUpToDate(); - }); - - it.skip("Add the Slice to the Custom Type", () => { - cy.visit(`/custom-types/${customTypeId}`); - - cy.get("[data-cy=update-slices]").click(); - cy.get(`[data-cy=shared-slice-card-${sliceId}]`).click(); - cy.get("[data-cy=update-slices-modal]").submit(); - - customTypeBuilder.save(); - - cy.reload(); - cy.contains(sliceName); - }); - - it.skip("Push the custom type with the Slice associated", () => { - changesPage.goTo().pushChanges().isUpToDate(); - }); - - it.skip("Displays and fill the satisfaction survey and the survey never reappears after", () => { - const lastSyncChange = Date.now() - 1000 * 60 * 60 * 2; - - // Setting the context to display the survey - cy.setSliceMachineUserContext({ - hasSendAReview: false, - lastSyncChange, - }); - - // Visit a page - cy.visit("/"); - - cy.get("#review-form"); - cy.get("[data-cy=review-form-score-3]").click(); - cy.get("[data-cy=review-form-comment]").type( - "Cypress test - testing the comment of the survey", - ); - cy.get("#review-form").submit(); - - cy.reload(); - cy.waitUntil(() => cy.get("[data-cy=create-ct]")); - - cy.get("#review-form").should("not.exist"); - }); }); diff --git a/cypress/e2e/user-flows/transactional-push.cy.js b/cypress/e2e/user-flows/transactional-push.cy.js deleted file mode 100644 index f0cf62ef55..0000000000 --- a/cypress/e2e/user-flows/transactional-push.cy.js +++ /dev/null @@ -1,140 +0,0 @@ -import { sliceBuilder } from "../../pages/slices/sliceBuilder"; -import { menu } from "../../pages/menu"; -import { customTypeBuilder } from "../../pages/customTypes/customTypeBuilder"; -import { slicesList } from "../../pages/slices/slicesList"; -import { changesPage } from "../../pages/changes/changesPage"; -import { - hardDeleteDocumentsDrawer, - referencesErrorDrawer, - softDeleteDocumentsDrawer, -} from "../../pages/changes/drawers"; - -// TODO: enable when transactional push requests responses can be mocked -describe.skip("I am an existing SM user and I want to push local changes", () => { - const random = Date.now(); - - const slice = { - id: `test_push${random}`, - name: `TestPush${random}`, - library: ".--slices", - }; - - const customType = { - id: `push_ct_${random}`, - name: `Push CT ${random}`, - }; - - beforeEach("Start from the Slice page", () => { - cy.clearProject(); - cy.setSliceMachineUserContext({}); - }); - - it("Creates, updates and deletes slices and custom types", () => { - cy.createSlice(slice.library, slice.id, slice.name); - cy.createCustomType(customType.id, customType.name); - - menu.navigateTo("Changes"); - - changesPage.pushChanges().isUpToDate(); - - customTypeBuilder.goTo(customType.id).addTab("Foo").save(); - - sliceBuilder.goTo(slice.library, slice.name).addVariation("foo").save(); - - menu.navigateTo("Changes"); - - changesPage.pushChanges(2); - - cy.deleteCustomType(customType.id); - - menu.navigateTo("Slices"); - - slicesList.deleteSlice(slice.name); - - menu.navigateTo("Changes"); - - const customTypesWithDocuments = [ - { - id: customType.id, - numberOfDocuments: 2000, - url: "url", - }, - ]; - - changesPage.mockPushLimit("HARD", customTypesWithDocuments); - - changesPage.pushChanges(); - - hardDeleteDocumentsDrawer.title.should("be.visible"); - hardDeleteDocumentsDrawer - .getAssociatedDocuments(customType.name) - .contains(customTypesWithDocuments[0].numberOfDocuments); - - changesPage.mockPushLimit("SOFT", customTypesWithDocuments); - - hardDeleteDocumentsDrawer.pushButton.click(); - - softDeleteDocumentsDrawer.title.should("be.visible"); - softDeleteDocumentsDrawer - .getAssociatedDocuments(customType.name) - .contains(customTypesWithDocuments[0].numberOfDocuments); - - softDeleteDocumentsDrawer.pushButton.should("be.disabled"); - - changesPage.unMockPushRequest(); - - softDeleteDocumentsDrawer.confirmDelete(); - - changesPage.isUpToDate(); - }); - - it("shows a toaster on error", () => { - cy.createCustomType(customType.id, customType.name); - - cy.visit("/changes"); - - changesPage.mockPushError(500).pushChanges(); - - cy.contains( - "Something went wrong when pushing your changes. Check your terminal logs.", - ); - - cy.clearProject(); - }); - - it("show's the login modal on auth error", () => { - cy.createCustomType(customType.id, customType.name); - - cy.visit("/changes"); - - changesPage.mockPushError(403).pushChanges(); - - cy.get("[aria-modal]").contains("You're not connected"); - cy.get("[aria-modal]").get("[aria-label='Close']").click(); - - cy.clearProject(); - }); - - it("show removed slice references", async () => { - cy.createSlice(slice.library, slice.id, slice.name); - cy.createCustomType(customType.id, customType.name); - - customTypeBuilder.goTo(customType.id); - - await customTypeBuilder.addSliceToSliceZone(slice.id).save(); - - cy.clearSlices(); - - menu.navigateTo("Changes"); - changesPage.pushChanges(); - - referencesErrorDrawer.title.should("be.visible"); - referencesErrorDrawer.getCustomTypeReferencesCard(customType.name); - - customTypeBuilder.goTo(customType.id).save(); - - menu.navigateTo("Changes"); - - changesPage.pushChanges().isUpToDate(); - }); -}); 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/CustomTypeBuilder/index.tsx b/packages/slice-machine/lib/builders/CustomTypeBuilder/index.tsx index a60f892b0c..9497adc544 100644 --- a/packages/slice-machine/lib/builders/CustomTypeBuilder/index.tsx +++ b/packages/slice-machine/lib/builders/CustomTypeBuilder/index.tsx @@ -57,6 +57,7 @@ export const CustomTypeBuilder: FC = (props) => { onAddNewTab={() => { setDialog({ type: "CREATE_CUSTOM_TYPE" }); }} + iconButtonTestId={{ "data-cy": "add-tab-button" }} > {customType.tabs.map((tab) => ( - 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/packages/slice-machine/src/components/Window/Window.tsx b/packages/slice-machine/src/components/Window/Window.tsx index a0e1f776f7..84cf56556f 100644 --- a/packages/slice-machine/src/components/Window/Window.tsx +++ b/packages/slice-machine/src/components/Window/Window.tsx @@ -36,17 +36,21 @@ export const WindowTabs: FC = (props) => ( ); -type WindowTabsListProps = PropsWithChildren<{ onAddNewTab?: () => void }>; +type WindowTabsListProps = PropsWithChildren<{ + onAddNewTab?: () => void; + iconButtonTestId?: { [name: string]: string }; +}>; export const WindowTabsList: FC = ({ children, onAddNewTab, + iconButtonTestId, ...otherProps }) => ( {children}
- +
); 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..dc7f075f2a 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,19 +72,45 @@ 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 user not onboarded: -Example for a logged in user not onboarded: +```ts +test.run({ onboarded: false })("I can ...", + async ({ sliceBuilderPage, slicesListPage }) => { + // Test content + }); +``` + +You can also override default storage values: + +Example (redux): +```ts +test.run({ + onboarded: false, + reduxStorage: { + lastSyncChange: new Date().getTime(), + }, +})("I can ...", + async ({ sliceBuilderPage, slicesListPage }) => { + // Test content + }); +``` +Example (new way): ```ts -test.run({ loggedIn: true, onboarded: false })("I can ...", +test.run({ + onboarded: false, + storage: { + isInAppGuideOpen: true, + }, +})("I can ...", async ({ sliceBuilderPage, slicesListPage }) => { // Test content }); ``` -Warning: Only use `loggedIn: true` when it's necessary for your test, it increases local test time (not in CI) by some seconds (≃ 3 secs). ### Mocking with `mockManagerProcedures` @@ -109,7 +128,7 @@ await mockManagerProcedures({ path: "getState", data: (data) => ({ ...data, - libraries: emptyLibraries, + libraries: generateLibraries({ nbSlices: 0 }), customTypes: [], remoteCustomTypes: [], remoteSlices: [], @@ -123,7 +142,7 @@ await mockManagerProcedures({ }); ``` -Warning: Only mock when it's necessary because the state of Slice Machine or the remote repository can change. +**Warning**: Only mock when it's necessary because the state of Slice Machine or the remote repository can change. 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. @@ -212,7 +231,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..f1f372c28f 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 @@ -47,9 +43,13 @@ export type DefaultFixtures = { * Default test fixture */ export const defaultTest = ( - options: { loggedIn?: boolean; onboarded?: boolean } = {}, + options: { + onboarded?: boolean; + reduxStorage?: Record; + storage?: Record; + } = {}, ) => { - const { loggedIn = false, onboarded = true } = options; + const { onboarded = true, reduxStorage = {}, storage = {} } = options; return baseTest.extend({ /** @@ -149,61 +149,84 @@ export const defaultTest = ( * Page */ page: async ({ browser }, use) => { - // Onboard user in Local Storage by default - let context = await browser.newContext({ - storageState: { - cookies: [], - origins: [ + // Create redux storage state + const userContext = onboarded + ? { + userReview: { + onboarding: false, + advancedRepository: true, + }, + updatesViewed: { + latest: null, + latestNonBreaking: null, + }, + hasSeenChangesToolTip: true, + hasSeenSimulatorToolTip: true, + hasSeenTutorialsToolTip: true, + authStatus: "unknown", + lastSyncChange: null, + ...reduxStorage, + } + : { + userReview: { + onboarding: onboarded, + advancedRepository: false, + }, + updatesViewed: { + latest: null, + latestNonBreaking: null, + }, + hasSeenChangesToolTip: false, + hasSeenSimulatorToolTip: false, + hasSeenTutorialsToolTip: false, + authStatus: "unknown", + lastSyncChange: null, + ...reduxStorage, + }; + + // Create new storage state + const SLICE_MACHINE_STORAGE_PREFIX = "slice-machine"; + const storageFormatted = Object.entries(storage).map(([key, value]) => ({ + name: `${SLICE_MACHINE_STORAGE_PREFIX}_${key}`, + value: JSON.stringify(value), + })); + const newStorage = onboarded + ? [ { - origin: config.use.baseURL, - localStorage: [ - { - name: "persist:root", - value: JSON.stringify({ - userContext: { - userReview: { - onboarding: true, - advancedRepository: true, - }, - updatesViewed: { - latest: null, - latestNonBreaking: null, - }, - hasSeenChangesToolTip: true, - hasSeenSimulatorToolTip: true, - hasSeenTutorialsToolTip: true, - authStatus: "unknown", - lastSyncChange: null, - }, - }), - }, - { - name: "slice-machine_isInAppGuideOpen", - value: "false", - }, - ], + name: `${SLICE_MACHINE_STORAGE_PREFIX}_isInAppGuideOpen`, + value: "false", }, - ], - }, - }); + ] + .filter( + (item) => + storageFormatted.findIndex( + (formattedItem) => formattedItem.name === item.name, + ) === -1, + ) + .concat(storageFormatted) + : storageFormatted; - // Prevent user to be onboarded if needed - if (!onboarded) { - context = await browser.newContext({ - storageState: { - cookies: [], - origins: [ + // Onboard user in Local Storage by default + const storageState = { + cookies: [], + origins: [ + { + origin: config.use.baseURL, + localStorage: [ { - origin: config.use.baseURL, - localStorage: [], + name: "persist:root", + value: JSON.stringify({ + userContext: JSON.stringify(userContext), + }), }, - ], + ].concat(newStorage), }, - }); - } + ], + }; // Create new page object with new context - const page = await context.newPage(); + const newContext = await browser.newContext({ storageState }); + const page = await newContext.newPage(); // Logout user by default try { @@ -212,99 +235,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/mocks/emptyLibraries.ts b/playwright/mocks/emptyLibraries.ts deleted file mode 100644 index 900f370f4c..0000000000 --- a/playwright/mocks/emptyLibraries.ts +++ /dev/null @@ -1,13 +0,0 @@ -export const emptyLibraries = [ - { - name: "slices", - path: "slices", - isLocal: true, - components: [], - meta: { - isNodeModule: false, - isDownloaded: false, - isManual: true, - }, - }, -]; diff --git a/playwright/mocks/generateLibraries.ts b/playwright/mocks/generateLibraries.ts new file mode 100644 index 0000000000..e84ab35ac4 --- /dev/null +++ b/playwright/mocks/generateLibraries.ts @@ -0,0 +1,86 @@ +import { SharedSlice } from "@prismicio/types-internal/lib/customtypes"; + +import { generateRandomId } from "../utils"; + +type GenerateTypesArgs = { nbSlices: number }; + +export type Library = { + name: string; + path: string; + isLocal: boolean; + components: { + from: string; + href: string; + pathToSlice: string; + fileName: string | null; + extension: string | null; + model: SharedSlice; + screenshots: Record< + string, + { + hash: string; + data: Buffer; + } + >; + }[]; + meta: { + name?: string; + version?: string; + isNodeModule: boolean; + isDownloaded: boolean; + isManual: boolean; + }; +}; + +export function generateLibraries(args: GenerateTypesArgs): Library[] { + const { nbSlices } = args; + + return [ + { + name: "slices", + path: "slices", + isLocal: true, + components: Array.from({ length: nbSlices }, (_, n) => ({ + from: "slices", + href: "slices", + pathToSlice: "pathToSlice", + fileName: "fileName", + extension: "extension", + model: { + id: `test_slice_${n}_${generateRandomId()}`, + type: "SharedSlice", + name: `TestSlice${n}${generateRandomId()}`, + description: "ExternalVideoSlice", + variations: [ + { + id: `default`, + name: `Default`, + docURL: "...", + version: "sktwi1xtmkfgx8626", + description: "ExternalVideoSlice", + primary: { + video: { + type: "IntegrationFields", + config: { + catalog: "demo-sm-next-ecom--external_videos", + label: "Video", + }, + }, + }, + items: {}, + imageUrl: + "https://images.prismic.io/slice-machine/621a5ec4-0387-4bc5-9860-2dd46cbc07cd_default_ss.png?auto=compress,format", + }, + ], + }, + screenshots: {}, + mocks: [], + })), + meta: { + isNodeModule: false, + isDownloaded: false, + isManual: true, + }, + }, + ]; +} diff --git a/playwright/mocks/generateTypes.ts b/playwright/mocks/generateTypes.ts new file mode 100644 index 0000000000..061d71b679 --- /dev/null +++ b/playwright/mocks/generateTypes.ts @@ -0,0 +1,48 @@ +import { CustomType } from "@prismicio/types-internal/lib/customtypes"; + +import { Library } from "./generateLibraries"; +import { generateRandomId } from "../utils"; + +type GenerateTypesArgs = { + nbTypes: number; + format?: "custom" | "page"; + libraries?: Library[]; +}; + +export function generateTypes(args: GenerateTypesArgs): CustomType[] { + const { nbTypes, format = "page", libraries } = args; + + return Array.from( + { length: nbTypes }, + (_, n): CustomType => ({ + id: `MyType${n}ID${generateRandomId()}`, + label: `MyType${n}Label${generateRandomId()}`, + repeatable: false, + status: true, + json: { + Main: { + title: { + type: "Text", + config: { label: "Title", placeholder: "" }, + }, + ...(libraries + ? { + slices: { + type: "Slices", + fieldset: "Slice Zone", + config: { + choices: { + [libraries[0]?.components[0]?.model.id as string]: { + type: "SharedSlice", + }, + }, + }, + }, + } + : {}), + }, + }, + format, + }), + ); +} diff --git a/playwright/mocks/index.ts b/playwright/mocks/index.ts index 63026fc81c..0a0c2122df 100644 --- a/playwright/mocks/index.ts +++ b/playwright/mocks/index.ts @@ -1,2 +1,2 @@ -export { emptyLibraries } from "./emptyLibraries"; -export { simpleCustomType } from "./simpleCustomType"; +export { generateLibraries } from "./generateLibraries"; +export { generateTypes } from "./generateTypes"; diff --git a/playwright/mocks/simpleCustomType.ts b/playwright/mocks/simpleCustomType.ts deleted file mode 100644 index 41be73139b..0000000000 --- a/playwright/mocks/simpleCustomType.ts +++ /dev/null @@ -1,15 +0,0 @@ -export const simpleCustomType = { - id: "footer", - label: "Footer", - repeatable: false, - status: true, - json: { - Main: { - title: { - type: "Text", - config: { label: "Title", placeholder: "" }, - }, - }, - }, - format: "custom", -}; diff --git a/playwright/package.json b/playwright/package.json index 1687f5b04d..13531c5f22 100644 --- a/playwright/package.json +++ b/playwright/package.json @@ -17,10 +17,10 @@ "devDependencies": { "@msgpack/msgpack": "2.8.0", "@playwright/test": "1.39.0", + "@prismicio/types-internal": "2.2.0", "@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/ChangesPage.ts b/playwright/pages/ChangesPage.ts index 4fca088369..2d89da780e 100644 --- a/playwright/pages/ChangesPage.ts +++ b/playwright/pages/ChangesPage.ts @@ -12,6 +12,12 @@ export class ChangesPage extends SliceMachinePage { readonly notLoggedInTitle: Locator; readonly notAuthorizedTitle: Locator; readonly blankSlateTitle: Locator; + readonly unknownErrorMessage: Locator; + readonly softLimitTitle: Locator; + readonly softLimitCheckbox: Locator; + readonly softLimitButton: Locator; + readonly hardLimitTitle: Locator; + readonly hardLimitButton: Locator; constructor(page: Page) { super(page); @@ -45,6 +51,29 @@ export class ChangesPage extends SliceMachinePage { this.blankSlateTitle = page.getByText("Everything up-to-date", { exact: true, }); + this.unknownErrorMessage = page.getByText( + "Something went wrong when pushing your changes. Check your terminal logs.", + { + exact: true, + }, + ); + this.softLimitTitle = page.getByText("Confirm deletion", { + exact: true, + }); + this.softLimitCheckbox = page.getByText("Delete these Documents", { + exact: true, + }); + this.softLimitButton = page.getByRole("button", { + name: "Push changes", + exact: true, + }); + this.hardLimitTitle = page.getByText("Manual action required", { + exact: true, + }); + this.hardLimitButton = page.getByRole("button", { + name: "Try again", + exact: true, + }); } /** @@ -65,7 +94,14 @@ export class ChangesPage extends SliceMachinePage { async pushChanges() { await this.pushChangesButton.click(); await expect(this.pushChangesButton).toBeDisabled(); - await expect(this.pushedMessaged).toBeVisible(); + await this.checkPushedMessage(); + } + + async confirmDeleteDocuments() { + await this.softLimitCheckbox.click(); + await this.softLimitButton.click(); + await expect(this.softLimitTitle).not.toBeVisible(); + await this.checkPushedMessage(); } /** @@ -88,4 +124,9 @@ export class ChangesPage extends SliceMachinePage { this.getCustomType(id).getByText(status, { exact: true }), ).toBeVisible(); } + + async checkPushedMessage() { + await expect(this.pushedMessaged).toBeVisible(); + await expect(this.pushedMessaged).not.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..3054b5074c 100644 --- a/playwright/pages/SliceMachinePage.ts +++ b/playwright/pages/SliceMachinePage.ts @@ -1,11 +1,13 @@ -import { Locator, Page } from "@playwright/test"; +import { Locator, Page, expect } 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); /** @@ -37,6 +40,7 @@ export class SliceMachinePage { */ async gotoDefaultPage() { await this.page.goto("/"); + await expect(this.breadcrumb).toBeVisible(); } /** diff --git a/playwright/pages/components/AddTabDialog.ts b/playwright/pages/components/AddTabDialog.ts new file mode 100644 index 0000000000..f70e9a5979 --- /dev/null +++ b/playwright/pages/components/AddTabDialog.ts @@ -0,0 +1,47 @@ +import { expect, Locator, Page } from "@playwright/test"; + +import { Dialog } from "./Dialog"; + +export class AddTabDialog extends Dialog { + readonly idInput: Locator; + + constructor(page: Page) { + super(page, { + title: "Add Tab", + submitName: "Save", + }); + + /** + * Components + */ + // Handle components here + + /** + * Static locators + */ + this.idInput = this.dialog.getByPlaceholder( + "A label for selecting the tab (i.e. not used in the API)", + { exact: true }, + ); + } + + /** + * Dynamic locators + */ + // Handle dynamic locators here + + /** + * Actions + */ + async createTab(name: string) { + await expect(this.title).toBeVisible(); + await this.idInput.fill(name); + await this.submitButton.click(); + await expect(this.title).not.toBeVisible(); + } + + /** + * Assertions + */ + // Handle assertions here +} diff --git a/playwright/pages/components/CreateSliceDialog.ts b/playwright/pages/components/CreateSliceDialog.ts index 730fc0f242..38b698fe33 100644 --- a/playwright/pages/components/CreateSliceDialog.ts +++ b/playwright/pages/components/CreateSliceDialog.ts @@ -5,6 +5,7 @@ import { Dialog } from "./Dialog"; export class CreateSliceDialog extends Dialog { readonly createdMessage: Locator; readonly nameInput: Locator; + readonly sliceAlreadyExistMessage: Locator; constructor(page: Page) { super(page, { @@ -24,6 +25,10 @@ export class CreateSliceDialog extends Dialog { exact: false, }); this.nameInput = this.dialog.getByTestId("slice-name-input"); + this.sliceAlreadyExistMessage = this.dialog.getByText( + "Slice name is already taken.", + { exact: true }, + ); } /** 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..4efe92ed38 --- /dev/null +++ b/playwright/pages/components/ReviewDialog.ts @@ -0,0 +1,49 @@ +import { Locator, Page, expect } from "@playwright/test"; + +import { Dialog } from "./Dialog"; + +export class ReviewDialog extends Dialog { + readonly messageTextarea: Locator; + + constructor(page: Page) { + super(page, { + title: "Share feedback", + }); + + /** + * Components + */ + // Handle components here + + /** + * Static locators + */ + this.messageTextarea = this.dialog.getByPlaceholder("Tell us more...", { + exact: true, + }); + } + + /** + * Dynamic locators + */ + // Handle dynamic locators here + + /** + * Actions + */ + async submitReview(args: { rating: number; message: string }) { + const { rating, message } = args; + await expect(this.title).toBeVisible(); + await this.dialog + .getByRole("button", { name: rating.toString(), exact: true }) + .click(); + await this.messageTextarea.fill(message); + await this.submitButton.click(); + await expect(this.title).not.toBeVisible(); + } + + /** + * 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..29add450ba 100644 --- a/playwright/pages/shared/TypeBuilderPage.ts +++ b/playwright/pages/shared/TypeBuilderPage.ts @@ -3,20 +3,25 @@ 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 { AddTabDialog } from "../components/AddTabDialog"; import { CustomTypesTablePage } from "../CustomTypesTablePage"; -import { BuilderPage } from "./BuilderPage"; import { PageTypesTablePage } from "../PageTypesTablePage"; +import { BuilderPage } from "./BuilderPage"; export class TypeBuilderPage extends BuilderPage { readonly createTypeDialog: CreateTypeDialog; readonly renameTypeDialog: RenameTypeDialog; readonly useTemplateSlicesDialog: UseTemplateSlicesDialog; + readonly selectExistingSlicesDialog: SelectExistingSlicesDialog; + readonly addTabDialog: AddTabDialog; readonly customTypeTablePage: CustomTypesTablePage; readonly pageTypeTablePage: PageTypesTablePage; readonly format: "page" | "custom"; readonly savedMessage: Locator; readonly tab: Locator; readonly tabList: Locator; + readonly addTabButton: Locator; readonly staticZone: Locator; readonly staticZonePlaceholder: Locator; readonly staticZoneListItem: Locator; @@ -24,6 +29,7 @@ export class TypeBuilderPage extends BuilderPage { readonly sliceZoneBlankSlate: Locator; readonly sliceZoneBlankSlateTitle: Locator; readonly sliceZoneUseTemplateAction: Locator; + readonly sliceZoneSelectExistingAction: Locator; readonly sliceZoneSharedSliceCard: Locator; constructor( @@ -41,8 +47,10 @@ 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); + this.addTabDialog = new AddTabDialog(page); /** * Static locators @@ -58,6 +66,7 @@ export class TypeBuilderPage extends BuilderPage { // Tabs this.tabList = page.getByRole("tablist"); this.tab = this.tabList.getByRole("tab"); + this.addTabButton = this.tabList.getByTestId("add-tab-button"); // Static zone this.staticZone = page.getByTestId("ct-static-zone"); this.staticZonePlaceholder = this.staticZone.getByText( @@ -77,6 +86,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"); } @@ -116,6 +128,12 @@ export class TypeBuilderPage extends BuilderPage { await typePage.getRow(name).click(); } + async openTab(name: string) { + await expect(this.getTab(name)).toBeVisible(); + await this.getTab(name).click(); + await this.checkIfTabIsActive(name); + } + /** * Assertions */ @@ -123,4 +141,8 @@ export class TypeBuilderPage extends BuilderPage { await expect(this.savedMessage).toBeVisible(); await expect(this.savedMessage).not.toBeVisible(); } + + async checkIfTabIsActive(name: string) { + await expect(this.getTab(name)).toHaveAttribute("aria-selected", "true"); + } } diff --git a/playwright/playwright.config.ts b/playwright/playwright.config.ts index f9d6f0037e..3309beeaae 100644 --- a/playwright/playwright.config.ts +++ b/playwright/playwright.config.ts @@ -9,11 +9,8 @@ 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, + // Run tests in parallel even within a single file. + fullyParallel: true, // Configure projects for major browsers. projects: [ @@ -25,6 +22,9 @@ const config = { }, ], + // Retry on CI only. + retries: process.env["CI"] ? 2 : 0, + // Reporter to use. reporter: process.env["CI"] ? [["github"], ["html"]] @@ -80,10 +80,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..df70d32df3 100644 --- a/playwright/tests/changes/changes.spec.ts +++ b/playwright/tests/changes/changes.spec.ts @@ -1,13 +1,27 @@ +import { CustomType } from "@prismicio/types-internal/lib/customtypes"; import { expect } from "@playwright/test"; import { test } from "../../fixtures"; import { mockManagerProcedures } from "../../utils"; -import { emptyLibraries, simpleCustomType } from "../../mocks"; +import { generateLibraries, generateTypes } 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 +32,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 +77,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({ @@ -58,10 +87,11 @@ test.describe("Changes", () => { path: "getState", data: (data) => ({ ...data, - libraries: emptyLibraries, + libraries: generateLibraries({ nbSlices: 0 }), customTypes: [], remoteCustomTypes: [], remoteSlices: [], + clientError: undefined, }), }, ], @@ -73,9 +103,11 @@ test.describe("Changes", () => { }, ); - test.run({ loggedIn: true })( + test.run()( "I can see the changes I have to push", async ({ changesPage }) => { + const types = generateTypes({ nbTypes: 1 }); + const customType = types[0] as CustomType; await mockManagerProcedures({ page: changesPage.page, procedures: [ @@ -83,10 +115,11 @@ test.describe("Changes", () => { path: "getState", data: (data) => ({ ...data, - libraries: emptyLibraries, - customTypes: [simpleCustomType], + libraries: generateLibraries({ nbSlices: 0 }), + customTypes: types, remoteCustomTypes: [], remoteSlices: [], + clientError: undefined, }), }, ], @@ -95,16 +128,117 @@ test.describe("Changes", () => { await changesPage.goto(); await expect(changesPage.loginButton).not.toBeVisible(); await changesPage.checkCustomTypeName( - simpleCustomType.id, - simpleCustomType.label, + customType.id, + customType.label as string, ); - await changesPage.checkCustomTypeApiId(simpleCustomType.id); - await changesPage.checkCustomTypeStatus(simpleCustomType.id, "New"); + await changesPage.checkCustomTypeApiId(customType.id); + await changesPage.checkCustomTypeStatus(customType.id, "New"); + }, + ); + + test.run()("I can push the changes I have", async ({ changesPage }) => { + await mockManagerProcedures({ + page: changesPage.page, + procedures: [ + { + path: "getState", + data: (data) => ({ + ...data, + libraries: generateLibraries({ nbSlices: 0 }), + customTypes: generateTypes({ nbTypes: 1 }), + remoteCustomTypes: [], + remoteSlices: [], + clientError: undefined, + }), + }, + { + path: "prismicRepository.pushChanges", + execute: false, + }, + ], + }); + + await changesPage.goto(); + await expect(changesPage.loginButton).not.toBeVisible(); + await changesPage.pushChanges(); + }); + + test.run()( + "I can see an error when the push failed", + async ({ changesPage }) => { + await mockManagerProcedures({ + page: changesPage.page, + procedures: [ + { + path: "getState", + data: (data) => ({ + ...data, + libraries: generateLibraries({ nbSlices: 0 }), + customTypes: generateTypes({ nbTypes: 1 }), + remoteCustomTypes: [], + remoteSlices: [], + clientError: undefined, + }), + }, + { + path: "prismicRepository.pushChanges", + data: () => new Error("Error"), + execute: false, + }, + ], + }); + + await changesPage.goto(); + await expect(changesPage.loginButton).not.toBeVisible(); + await changesPage.pushChangesButton.click(); + await expect(changesPage.unknownErrorMessage).toBeVisible(); + }, + ); + + test.run()( + "I have to confirm the push when I reach a soft limit of deleted documents", + async ({ changesPage }) => { + await mockManagerProcedures({ + page: changesPage.page, + procedures: [ + { + path: "getState", + data: (data) => ({ + ...data, + libraries: generateLibraries({ nbSlices: 0 }), + customTypes: generateTypes({ nbTypes: 1 }), + remoteCustomTypes: [], + remoteSlices: [], + clientError: undefined, + }), + }, + { + path: "prismicRepository.pushChanges", + data: () => ({ + type: "SOFT", + details: { + customTypes: [], + }, + }), + execute: false, + }, + { + path: "prismicRepository.pushChanges", + execute: false, + }, + ], + }); + + await changesPage.goto(); + await expect(changesPage.loginButton).not.toBeVisible(); + await changesPage.pushChangesButton.click(); + await expect(changesPage.softLimitTitle).toBeVisible(); + await changesPage.confirmDeleteDocuments(); }, ); - test.run({ loggedIn: true })( - "I can push the changes I have", + test.run()( + "I cannot push the changes when I reach a hard limit of deleted documents", async ({ changesPage }) => { await mockManagerProcedures({ page: changesPage.page, @@ -113,11 +247,22 @@ test.describe("Changes", () => { path: "getState", data: (data) => ({ ...data, - libraries: emptyLibraries, - customTypes: [simpleCustomType], + libraries: generateLibraries({ nbSlices: 0 }), + customTypes: generateTypes({ nbTypes: 1 }), remoteCustomTypes: [], remoteSlices: [], + clientError: undefined, + }), + }, + { + path: "prismicRepository.pushChanges", + data: () => ({ + type: "HARD", + details: { + customTypes: [], + }, }), + execute: false, }, { path: "prismicRepository.pushChanges", @@ -128,7 +273,11 @@ test.describe("Changes", () => { await changesPage.goto(); await expect(changesPage.loginButton).not.toBeVisible(); - await changesPage.pushChanges(); + await changesPage.pushChangesButton.click(); + + await expect(changesPage.hardLimitTitle).toBeVisible(); + await changesPage.hardLimitButton.click(); + await changesPage.checkPushedMessage(); }, ); }); 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/reviewForm.spec.ts b/playwright/tests/common/reviewForm.spec.ts new file mode 100644 index 0000000000..99b2c060ad --- /dev/null +++ b/playwright/tests/common/reviewForm.spec.ts @@ -0,0 +1,158 @@ +import { expect } from "@playwright/test"; + +import { test } from "../../fixtures"; +import { mockManagerProcedures } from "../../utils"; +import { generateLibraries, generateTypes } from "../../mocks"; + +test.describe("Review form", () => { + test.run({ + onboarded: false, + reduxStorage: { + lastSyncChange: new Date(new Date().getTime() - 3600000).getTime(), + }, + })("I can write a review after onboarding", async ({ sliceMachinePage }) => { + const libraries = generateLibraries({ nbSlices: 1 }); + + // We mock a page type with a slice that is a requirement for the review dialog + await mockManagerProcedures({ + page: sliceMachinePage.page, + procedures: [ + { + path: "getState", + data: (data) => ({ + ...data, + libraries, + customTypes: generateTypes({ nbTypes: 1, libraries }), + remoteCustomTypes: [], + remoteSlices: [], + clientError: undefined, + }), + }, + ], + }); + + await sliceMachinePage.gotoDefaultPage(); + + // We close the in app guide to display the review dialog + await sliceMachinePage.inAppGuideDialog.closeButton.click(); + + await sliceMachinePage.reviewDialog.submitReview({ + rating: 4, + message: "Great job!", + }); + + // We verify that the review dialog is not displayed anymore + await sliceMachinePage.page.reload(); + await expect(sliceMachinePage.reviewDialog.title).not.toBeVisible(); + }); + + test.run({ + onboarded: false, + reduxStorage: { + lastSyncChange: new Date(new Date().getTime() - 3600000).getTime(), + }, + })( + "I can write a review after creating enough models", + async ({ sliceMachinePage }) => { + const libraries = generateLibraries({ nbSlices: 6 }); + + // We mock a page type with a slice that is a requirement for the review dialog + await mockManagerProcedures({ + page: sliceMachinePage.page, + procedures: [ + { + path: "getState", + data: (data) => ({ + ...data, + libraries, + customTypes: generateTypes({ nbTypes: 1, libraries }), + remoteCustomTypes: [], + remoteSlices: [], + clientError: undefined, + }), + }, + { + path: "getState", + data: (data) => ({ + ...data, + libraries, + customTypes: generateTypes({ nbTypes: 1, libraries }), + remoteCustomTypes: [], + remoteSlices: [], + clientError: undefined, + }), + }, + { + path: "getState", + data: (data) => ({ + ...data, + libraries, + customTypes: generateTypes({ nbTypes: 6, libraries }), + remoteCustomTypes: [], + remoteSlices: [], + clientError: undefined, + }), + }, + ], + }); + + await sliceMachinePage.gotoDefaultPage(); + + // We close the in app guide so the review dialog can be displayed + await sliceMachinePage.inAppGuideDialog.closeButton.click(); + + // We close the first review for onboarding + await sliceMachinePage.reviewDialog.closeButton.click(); + + // We reload and mock with 6 types to trigger the advanced review dialog + await sliceMachinePage.page.reload(); + + await sliceMachinePage.reviewDialog.submitReview({ + rating: 5, + message: "I love Slice Machine!", + }); + + // We verify that the review dialog is not displayed anymore + await sliceMachinePage.page.reload(); + await expect(sliceMachinePage.reviewDialog.title).not.toBeVisible(); + }, + ); + + test.run({ + onboarded: false, + reduxStorage: { + lastSyncChange: new Date(new Date().getTime() - 3600000).getTime(), + }, + })("I can close the review dialog", async ({ sliceMachinePage }) => { + const libraries = generateLibraries({ nbSlices: 1 }); + + // We mock a page type with a slice that is a requirement for the review dialog + await mockManagerProcedures({ + page: sliceMachinePage.page, + procedures: [ + { + path: "getState", + data: (data) => ({ + ...data, + libraries, + customTypes: generateTypes({ nbTypes: 1, libraries }), + remoteCustomTypes: [], + remoteSlices: [], + clientError: undefined, + }), + }, + ], + }); + + await sliceMachinePage.gotoDefaultPage(); + + // We close the in app guide to display the review dialog + await sliceMachinePage.inAppGuideDialog.closeButton.click(); + + await sliceMachinePage.reviewDialog.closeButton.click(); + + // We verify that the review dialog is not displayed anymore + await sliceMachinePage.page.reload(); + await expect(sliceMachinePage.reviewDialog.title).not.toBeVisible(); + }); +}); diff --git a/playwright/tests/common/sideNav.spec.ts b/playwright/tests/common/sideNav.spec.ts new file mode 100644 index 0000000000..81776eb286 --- /dev/null +++ b/playwright/tests/common/sideNav.spec.ts @@ -0,0 +1,173 @@ +import { expect } from "@playwright/test"; + +import { test } from "../../fixtures"; +import { mockManagerProcedures } from "../../utils/mockManagerProcedures"; +import { generateLibraries, generateTypes } from "../../mocks"; + +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, + reduxStorage: { + lastSyncChange: new Date(new Date().getTime() - 3600000).getTime(), + }, + })( + "I can close the tutorial video tooltip and it stays close", + async ({ sliceMachinePage }) => { + const libraries = generateLibraries({ nbSlices: 1 }); + + // We mock a page type with a slice that is a requirement for the review dialog + await mockManagerProcedures({ + page: sliceMachinePage.page, + procedures: [ + { + path: "getState", + data: (data) => ({ + ...data, + libraries, + customTypes: generateTypes({ nbTypes: 1, libraries }), + remoteCustomTypes: [], + remoteSlices: [], + clientError: undefined, + }), + }, + ], + }); + + await sliceMachinePage.gotoDefaultPage(); + + // 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(sliceMachinePage.menu.pageTypesLink).toBeVisible(); + + await expect( + sliceMachinePage.menu.tutorialVideoTooltipTitle, + ).not.toBeVisible(); + }, + ); +}); diff --git a/playwright/tests/pageTypes/pageTypeBuilder.spec.ts b/playwright/tests/pageTypes/pageTypeBuilder.spec.ts index c72b75bd57..9f721377d0 100644 --- a/playwright/tests/pageTypes/pageTypeBuilder.spec.ts +++ b/playwright/tests/pageTypes/pageTypeBuilder.spec.ts @@ -33,4 +33,26 @@ test.describe("Page types builder", () => { ).toBeVisible(); }, ); + + test.run()( + "I cannot add slices in SEO & Metadata tab by default", + async ({ pageTypesBuilderPage, reusablePageType }) => { + await pageTypesBuilderPage.goto(reusablePageType.name); + await pageTypesBuilderPage.openTab("SEO & Metadata"); + + await expect(pageTypesBuilderPage.sliceZoneSwitch).not.toBeChecked(); + }, + ); + + test.run()( + "I cannot add slices in a new tab by default", + async ({ pageTypesBuilderPage, reusablePageType }) => { + await pageTypesBuilderPage.goto(reusablePageType.name); + await pageTypesBuilderPage.addTabButton.click(); + await pageTypesBuilderPage.addTabDialog.createTab("New tab"); + await pageTypesBuilderPage.checkIfTabIsActive("New tab"); + + await expect(pageTypesBuilderPage.sliceZoneSwitch).not.toBeChecked(); + }, + ); }); 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/tests/slices/slicesList.spec.ts b/playwright/tests/slices/slicesList.spec.ts index b6d62b81be..77e394c5b5 100644 --- a/playwright/tests/slices/slicesList.spec.ts +++ b/playwright/tests/slices/slicesList.spec.ts @@ -48,6 +48,23 @@ test.describe("Slices list", () => { }, ); + test.run()( + "I cannot create a slice with a name that already exists", + async ({ slicesListPage, slice }) => { + await slicesListPage.goto(); + await slicesListPage.openCreateDialog(); + + const { nameInput, submitButton } = slicesListPage.createSliceDialog; + + await nameInput.fill(slice.name); + await expect(submitButton).toBeDisabled(); + await submitButton.click({ force: true }); + await expect( + slicesListPage.createSliceDialog.sliceAlreadyExistMessage, + ).toBeVisible(); + }, + ); + test.run()( "I can rename a slice", async ({ slice, sliceBuilderPage, slicesListPage }) => { diff --git a/playwright/utils/mockManagerProcedures.ts b/playwright/utils/mockManagerProcedures.ts index 79a40f8c4b..ac958f4c26 100644 --- a/playwright/utils/mockManagerProcedures.ts +++ b/playwright/utils/mockManagerProcedures.ts @@ -19,7 +19,14 @@ 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 + | Error; /** * Whether to execute the procedure or not. Defaults to true. @@ -29,10 +36,13 @@ type MockManagerProceduresArgs = { }; /** - * Mocks manager procedures from SliceMachineManagerClient + * Mocks manager procedures from SliceMachineManagerClient. + * If you mock multiple procedures with the same path, they will be executed in order. + * The last one will be executed for all subsequent calls non directly covered. */ export async function mockManagerProcedures(args: MockManagerProceduresArgs) { const { page, procedures } = args; + let proceduresCountObject: Record = {}; await page.route("*/**/_manager", async (route) => { const postDataBuffer = route.request().postDataBuffer() as Buffer; @@ -40,13 +50,28 @@ export async function mockManagerProcedures(args: MockManagerProceduresArgs) { "procedurePath", unknown[] >; + const procedurePath = postData.procedurePath.join("."); + const procedureCount: number = proceduresCountObject[procedurePath] ?? 0; - const procedure = procedures.find( - (p) => p.path === postData.procedurePath.join("."), + const matchedProcedures = procedures.filter( + (p) => p.path === procedurePath, ); + let procedure = + matchedProcedures.length > 1 + ? matchedProcedures[procedureCount] + : matchedProcedures[0]; + + if (!procedure && matchedProcedures.length > 1) { + procedure = matchedProcedures[matchedProcedures.length - 1]; + } + if (procedure) { const { data, execute = true } = procedure; + proceduresCountObject = { + ...proceduresCountObject, + [procedure.path]: procedureCount + 1, + }; let newBody = Buffer.from( encode( diff --git a/yarn.lock b/yarn.lock index 4c3bb83b47..0e1f5504b0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8537,10 +8537,10 @@ __metadata: dependencies: "@msgpack/msgpack": 2.8.0 "@playwright/test": 1.39.0 + "@prismicio/types-internal": 2.2.0 "@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