diff --git a/.github/actions/restore-cache/action.yml b/.github/actions/restore-cache/action.yml
index 90c32680ce..4062a753a1 100644
--- a/.github/actions/restore-cache/action.yml
+++ b/.github/actions/restore-cache/action.yml
@@ -11,7 +11,6 @@ runs:
node_modules
*/*/node_modules
~/.cache/Cypress
- ~/.cache/ms-playwright
key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}-dependencies
- name: Cache artifacts
id: cache-artifacts
@@ -21,3 +20,10 @@ runs:
packages/*/dist
packages/core/build
key: ${{ runner.os }}-${{ github.run_id }}-artifacts
+ - name: Cache playwright binaries
+ id: cache-playwright-binaries
+ uses: actions/cache@v3
+ with:
+ path: |
+ ~/.cache/ms-playwright
+ key: ${{ runner.os }}-${{ hashFiles('playwright/package.json') }}-playwright-binaries
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e9e07148a4..68b016a833 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -152,6 +152,10 @@ jobs:
e2e:
needs: prepare
runs-on: [ubuntu-latest]
+ strategy:
+ fail-fast: false
+ matrix:
+ shard: [1/6, 2/6, 3/6, 4/6, 5/6, 6/6]
steps:
- name: Checkout
uses: actions/checkout@v3
@@ -165,20 +169,54 @@ jobs:
- name: Set up Playwright browsers
run: yarn test:e2e:install
+ - name: Set up Slice Machine project
+ run: |
+ yarn workspaces foreach --include '{@slicemachine/adapter-next,@slicemachine/manager,@slicemachine/plugin-kit,start-slicemachine}' --parallel --topological-dev run build
+ yarn workspace slice-machine-ui pack --out ../../e2e-projects/next/sm.tgz
+ yarn workspace cimsirp add -D ./sm.tgz
+
- name: Running E2E tests
- run: yarn test:e2e
+ run: yarn test:e2e --shard ${{ matrix.shard }}
env:
- PRISMIC_URL: https://wroom.io
- WROOM_EMAIL: ${{ secrets.EMAIL }}
- WROOM_PASSWORD: ${{ secrets.PASSWORD }}
+ DEBUG: pw:test
+
+ - name: Upload blob report to GitHub Actions Artifacts
+ uses: actions/upload-artifact@v3
+ if: always()
+ with:
+ name: e2e-all-blob-reports
+ path: playwright/blob-report
+ retention-days: 1
+
+ e2e-merge-reports:
+ needs: e2e
+ runs-on: ubuntu-latest
+ if: always()
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Restore cache
+ uses: ./.github/actions/restore-cache
+
+ - name: Set up Node
+ uses: ./.github/actions/setup-node
+
+ - name: Download blob reports from GitHub Actions Artifacts
+ uses: actions/download-artifact@v3
+ with:
+ name: e2e-all-blob-reports
+ path: playwright/e2e-all-blob-reports
+
+ - name: Merge into HTML Report
+ run: yarn test:e2e:merge-reports --reporter html ./e2e-all-blob-reports
- name: Upload HTML report
uses: actions/upload-artifact@v3
- if: failure()
with:
- name: Playwright HTML report
- path: playwright/playwright-report/
- retention-days: 30
+ name: html-report--attempt-${{ github.run_attempt }}
+ path: playwright/playwright-report
+ retention-days: 14
e2e-cypress:
needs: prepare
diff --git a/cypress/e2e/slices/00-create.cy.js b/cypress/e2e/slices/00-create.cy.js
index e0fb39ebba..f6ee653d86 100644
--- a/cypress/e2e/slices/00-create.cy.js
+++ b/cypress/e2e/slices/00-create.cy.js
@@ -10,7 +10,7 @@ const editedSliceName = "EditedSliceName";
const sliceId = "test_slice"; // generated automatically from the slice name
const lib = ".--slices";
-describe("Create Slices", () => {
+describe.skip("Create Slices", () => {
beforeEach(() => {
cy.setSliceMachineUserContext({});
cy.clearProject();
diff --git a/cypress/e2e/slices/01-duplicates.cy.js b/cypress/e2e/slices/01-duplicates.cy.js
index 39b105a1d9..5c0874eb48 100644
--- a/cypress/e2e/slices/01-duplicates.cy.js
+++ b/cypress/e2e/slices/01-duplicates.cy.js
@@ -2,7 +2,7 @@ const sliceName = "DuplicateSlices";
const sliceId = "duplicate_slices";
const lib = ".--slices";
-describe("Duplicate Slices", () => {
+describe.skip("Duplicate Slices", () => {
beforeEach(() => {
cy.setSliceMachineUserContext({});
cy.clearProject();
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
index e564b48f8a..0fdcffb874 100644
--- a/cypress/e2e/updates/simulator-tooltip.cy.js
+++ b/cypress/e2e/updates/simulator-tooltip.cy.js
@@ -20,7 +20,7 @@ describe("simulator tooltip", () => {
cy.contains("Simulate your slices").should("exist");
- cy.contains("Got It").click();
+ cy.contains("Got it").click();
cy.contains("Simulate your slices").should("not.exist");
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_custom_screenshots.cy.js b/cypress/e2e/user-flows/scenario_custom_screenshots.cy.js
index fc1308ca28..644f604f8d 100644
--- a/cypress/e2e/user-flows/scenario_custom_screenshots.cy.js
+++ b/cypress/e2e/user-flows/scenario_custom_screenshots.cy.js
@@ -4,7 +4,7 @@ import { menu } from "../../pages/menu";
import { sliceBuilder } from "../../pages/slices/sliceBuilder";
import { changesPage } from "../../pages/changes/changesPage";
-describe("I am an existing SM user and I want to upload screenshots on variations of an existing Slice", () => {
+describe.skip("I am an existing SM user and I want to upload screenshots on variations of an existing Slice", () => {
let slice;
const initSliceData = (random = Date.now()) => ({
id: `test_custom_screenshots${random}`,
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/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/.gitignore b/playwright/.gitignore
index 3b98d49bbe..53808aaf7d 100644
--- a/playwright/.gitignore
+++ b/playwright/.gitignore
@@ -1,3 +1,2 @@
-.env.local
/test-results/
/playwright-report/
diff --git a/playwright/README.md b/playwright/README.md
index de02a6a7d0..b098ed7766 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.
@@ -70,19 +63,17 @@ yarn test:e2e:report
To open a downloaded CI test report from anywhere in your computer:
-```bash
-npx playwright show-report name-of-my-extracted-playwright-report
-```
+- Copy-paste the content of the download report in a `playwright-report` folder within `playwright` folder
+- Execute the same command as above
## Creating tests
### `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 })(
@@ -93,7 +84,33 @@ test.run({ loggedIn: true, onboarded: false })(
);
```
-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).
+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({
+ onboarded: false,
+ storage: {
+ isInAppGuideOpen: true,
+ },
+})("I can ...",
+ async ({ sliceBuilderPage, slicesListPage }) => {
+ // Test content
+ },
+);
+```
### Mocking with `procedures.mock`
@@ -175,7 +192,8 @@ This approach has several benefits:
- Readability: Tests become more readable and easier to understand.
- Reusability: You can reuse code across different test cases.
-**Warning**: Never use a locator directly in a test file (getBy...). Always use the Page Object Model design pattern for that.
+> [!WARNING]
+> Never use a locator directly in a test file (getBy...). Always use the Page Object Model design pattern for that.
### Always try to do an exact matching with locators
@@ -251,6 +269,42 @@ test.run()("I can create a slice", async () => {
});
```
+### 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();
+ },
+);
+```
+
+> [!NOTE]
+> Include the check within the goto function so you don't need to do it manually every time.
+
### Write your own best practice for the team here...
## Useful links
diff --git a/playwright/fixtures/index.ts b/playwright/fixtures/index.ts
index f396ae3043..2a4884c770 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";
@@ -18,8 +16,6 @@ import { generateRandomId } from "../utils/generateRandomId";
import config from "../playwright.config";
import { MockManagerProcedures } from "../utils";
-dotenv.config({ path: `.env.local` });
-
export type DefaultFixtures = {
/**
* Pages
@@ -53,9 +49,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({
/**
@@ -155,61 +155,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 {
@@ -218,99 +241,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..368ad1506b
--- /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 = { slicesCount: 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 { slicesCount } = args;
+
+ return [
+ {
+ name: "slices",
+ path: "slices",
+ isLocal: true,
+ components: Array.from({ length: slicesCount }, (_, 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..9853eb0b1c
--- /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 = {
+ typesCount: number;
+ format?: "custom" | "page";
+ libraries?: Library[];
+};
+
+export function generateTypes(args: GenerateTypesArgs): CustomType[] {
+ const { typesCount, format = "page", libraries } = args;
+
+ return Array.from(
+ { length: typesCount },
+ (_, 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 6d4fc340d2..2da7f90710 100644
--- a/playwright/mocks/index.ts
+++ b/playwright/mocks/index.ts
@@ -1,3 +1,3 @@
-export { emptyLibraries } from "./emptyLibraries";
+export { generateLibraries } from "./generateLibraries";
+export { generateTypes } from "./generateTypes";
export { environments } from "./environments";
-export { simpleCustomType } from "./simpleCustomType";
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 83eebc84e2..0da38956e7 100644
--- a/playwright/package.json
+++ b/playwright/package.json
@@ -4,22 +4,23 @@
"description": "E2E tests for Slice Machine",
"author": "Prismic (https://prismic.io)",
"scripts": {
+ "audit": "echo \"No audit for playwright\" && true",
+ "depcheck": "depcheck --config=.depcheckrc",
"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",
"types": "tsc --noEmit",
- "unit": "echo \"No unit tests for playwright\" && true",
- "depcheck": "depcheck --config=.depcheckrc",
- "audit": "echo \"No audit for playwright\" && true"
+ "test:e2e:merge-reports": "playwright merge-reports",
+ "unit": "echo \"No unit tests for playwright\" && true"
},
"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/ChangesPage.ts b/playwright/pages/ChangesPage.ts
index b0644b407e..fd62c168f5 100644
--- a/playwright/pages/ChangesPage.ts
+++ b/playwright/pages/ChangesPage.ts
@@ -10,6 +10,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);
@@ -42,6 +48,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,
+ });
}
/**
@@ -62,7 +91,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();
}
/**
@@ -85,4 +121,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/SliceMachinePage.ts b/playwright/pages/SliceMachinePage.ts
index 2a614a59b9..399c7c75cc 100644
--- a/playwright/pages/SliceMachinePage.ts
+++ b/playwright/pages/SliceMachinePage.ts
@@ -1,12 +1,14 @@
-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";
import { LoginDialog } from "./components/LoginDialog";
export class SliceMachinePage {
readonly page: Page;
readonly menu: Menu;
+ readonly reviewDialog: ReviewDialog;
readonly inAppGuideDialog: InAppGuideDialog;
readonly loginDialog: LoginDialog;
readonly body: Locator;
@@ -18,6 +20,7 @@ export class SliceMachinePage {
*/
this.page = page;
this.menu = new Menu(page);
+ this.reviewDialog = new ReviewDialog(page);
this.inAppGuideDialog = new InAppGuideDialog(page);
this.loginDialog = new LoginDialog(page);
@@ -44,6 +47,7 @@ export class SliceMachinePage {
*/
async gotoDefaultPage() {
await this.page.goto("/");
+ await expect(this.breadcrumb).toBeVisible();
}
/**
diff --git a/playwright/pages/components/Menu.ts b/playwright/pages/components/Menu.ts
index c8cc0212d2..b13052bbfa 100644
--- a/playwright/pages/components/Menu.ts
+++ b/playwright/pages/components/Menu.ts
@@ -13,6 +13,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) {
/**
@@ -47,12 +51,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/playwright.config.ts b/playwright/playwright.config.ts
index f9d6f0037e..5ce4124817 100644
--- a/playwright/playwright.config.ts
+++ b/playwright/playwright.config.ts
@@ -12,9 +12,6 @@ const config = {
// 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,
-
// Configure projects for major browsers.
projects: [
{
@@ -25,9 +22,12 @@ const config = {
},
],
+ // Retry on CI only.
+ retries: process.env["CI"] ? 2 : 0,
+
// Reporter to use.
reporter: process.env["CI"]
- ? [["github"], ["html"]]
+ ? [["github"], ["blob"]]
: [
[
"html",
@@ -55,18 +55,23 @@ const config = {
baseURL: "http://localhost:9999",
// Collect trace when retrying the failed test.
- trace: "on",
- video: "off",
- screenshot: "on",
+ trace: "on-first-retry",
+
+ // Setup the test id attribute to be `data-cy` for `getByTestId`.
testIdAttribute: "data-cy",
+
+ // Configure the browser permissions to access the clipboard API.
+ permissions: ["clipboard-read", "clipboard-write"],
},
// Run local dev servers before starting the tests if needed.
webServer: [
{
cwd: "..",
- command: "yarn dev",
- url: `http://localhost:3000/`,
+ command: process.env["CI"] ? "yarn dev:e2e-next" : "yarn dev",
+ url: process.env["CI"]
+ ? "http://localhost:8000/"
+ : "http://localhost:3000/",
reuseExistingServer: !process.env["CI"],
stdout: "pipe",
timeout: 120_000,
@@ -74,16 +79,16 @@ const config = {
{
cwd: "../e2e-projects/next",
command: "yarn slicemachine:dev",
- url: `http://localhost:9999/`,
+ url: "http://localhost:9999/",
reuseExistingServer: !process.env["CI"],
stdout: "pipe",
timeout: 120_000,
},
],
- // Don't run tests in parallel due to the nature of
- // Slice Machine modifying file in the file system.
- workers: 1,
+ // Opt out of parallel tests on CI to prioritize stability and reproducibility.
+ // See: https://playwright.dev/docs/ci#workers
+ workers: process.env["CI"] ? 1 : undefined,
} satisfies PlaywrightTestConfig;
export default config;
diff --git a/playwright/tests/changes/changes.spec.ts b/playwright/tests/changes/changes.spec.ts
index dc87ac5def..a564dbe13c 100644
--- a/playwright/tests/changes/changes.spec.ts
+++ b/playwright/tests/changes/changes.spec.ts
@@ -1,12 +1,20 @@
+import { CustomType } from "@prismicio/types-internal/lib/customtypes";
import { expect } from "@playwright/test";
import { test } from "../../fixtures";
-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 }) => {
+ async ({ changesPage, procedures }) => {
+ procedures.mock("getState", ({ data }) => ({
+ ...(data as Record),
+ clientError: {
+ clientError: undefined,
+ },
+ }));
+
await changesPage.goto();
await expect(changesPage.loginButton).not.toBeVisible();
await expect(changesPage.notLoggedInTitle).not.toBeVisible();
@@ -16,14 +24,21 @@ test.describe("Changes", () => {
test.run()(
"I can see the login screen when logged out",
- async ({ changesPage }) => {
+ async ({ changesPage, procedures }) => {
+ procedures.mock("getState", ({ data }) => ({
+ ...(data as Record),
+ 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, procedures }) => {
procedures.mock("getState", ({ data }) => ({
@@ -39,15 +54,16 @@ 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, procedures }) => {
procedures.mock("getState", ({ data }) => ({
...(data as Record),
- libraries: emptyLibraries,
+ libraries: generateLibraries({ slicesCount: 0 }),
customTypes: [],
remoteCustomTypes: [],
remoteSlices: [],
+ clientError: undefined,
}));
await changesPage.goto();
@@ -56,37 +72,41 @@ test.describe("Changes", () => {
},
);
- test.run({ loggedIn: true })(
+ test.run()(
"I can see the changes I have to push",
async ({ changesPage, procedures }) => {
+ const types = generateTypes({ typesCount: 1 });
+ const customType = types[0] as CustomType;
procedures.mock("getState", ({ data }) => ({
...(data as Record),
- libraries: emptyLibraries,
- customTypes: [simpleCustomType],
+ libraries: generateLibraries({ slicesCount: 0 }),
+ customTypes: types,
remoteCustomTypes: [],
remoteSlices: [],
+ clientError: undefined,
}));
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({ loggedIn: true })(
+ test.run()(
"I can push the changes I have",
async ({ changesPage, procedures }) => {
procedures.mock("getState", ({ data }) => ({
...(data as Record),
- libraries: emptyLibraries,
- customTypes: [simpleCustomType],
+ libraries: generateLibraries({ slicesCount: 0 }),
+ customTypes: generateTypes({ typesCount: 1 }),
remoteCustomTypes: [],
remoteSlices: [],
+ clientError: undefined,
}));
procedures.mock("prismicRepository.pushChanges", () => undefined, {
execute: false,
@@ -97,4 +117,104 @@ test.describe("Changes", () => {
await changesPage.pushChanges();
},
);
+
+ test.run()(
+ "I can see an error when the push failed",
+ async ({ changesPage, procedures }) => {
+ procedures.mock("getState", ({ data }) => ({
+ ...(data as Record),
+ libraries: generateLibraries({ slicesCount: 0 }),
+ customTypes: generateTypes({ typesCount: 1 }),
+ remoteCustomTypes: [],
+ remoteSlices: [],
+ clientError: undefined,
+ }));
+ procedures.mock(
+ "prismicRepository.pushChanges",
+ () => 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, procedures }) => {
+ procedures.mock("getState", ({ data }) => ({
+ ...(data as Record),
+ libraries: generateLibraries({ slicesCount: 0 }),
+ customTypes: generateTypes({ typesCount: 1 }),
+ remoteCustomTypes: [],
+ remoteSlices: [],
+ clientError: undefined,
+ }));
+ procedures.mock(
+ "prismicRepository.pushChanges",
+ () => ({
+ type: "SOFT",
+ details: {
+ customTypes: [],
+ },
+ }),
+ {
+ execute: false,
+ },
+ );
+
+ await changesPage.goto();
+ await expect(changesPage.loginButton).not.toBeVisible();
+ await changesPage.pushChangesButton.click();
+ await expect(changesPage.softLimitTitle).toBeVisible();
+
+ procedures.mock("prismicRepository.pushChanges", () => undefined, {
+ execute: false,
+ });
+
+ await changesPage.confirmDeleteDocuments();
+ },
+ );
+
+ test.run()(
+ "I cannot push the changes when I reach a hard limit of deleted documents",
+ async ({ changesPage, procedures }) => {
+ procedures.mock("getState", ({ data }) => ({
+ ...(data as Record),
+ libraries: generateLibraries({ slicesCount: 0 }),
+ customTypes: generateTypes({ typesCount: 1 }),
+ remoteCustomTypes: [],
+ remoteSlices: [],
+ clientError: undefined,
+ }));
+
+ procedures.mock(
+ "prismicRepository.pushChanges",
+ () => ({
+ type: "HARD",
+ details: {
+ customTypes: [],
+ },
+ }),
+ { execute: false },
+ );
+
+ await changesPage.goto();
+ await expect(changesPage.loginButton).not.toBeVisible();
+ await changesPage.pushChangesButton.click();
+
+ procedures.mock("prismicRepository.pushChanges", () => undefined, {
+ execute: false,
+ });
+
+ 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..a0fb3dce86
--- /dev/null
+++ b/playwright/tests/common/reviewForm.spec.ts
@@ -0,0 +1,130 @@
+import { expect } from "@playwright/test";
+
+import { test } from "../../fixtures";
+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, procedures }) => {
+ const libraries = generateLibraries({ slicesCount: 1 });
+
+ // We mock a page type with a slice that is a requirement for the review dialog
+ procedures.mock("getState", ({ data }) => ({
+ ...(data as Record),
+ libraries,
+ customTypes: generateTypes({ typesCount: 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, procedures }) => {
+ const libraries = generateLibraries({ slicesCount: 6 });
+
+ // We mock a page type with a slice that is a requirement for the review dialog
+ procedures.mock(
+ "getState",
+ ({ data }) => ({
+ ...(data as Record),
+ libraries,
+ customTypes: generateTypes({ typesCount: 1, libraries }),
+ remoteCustomTypes: [],
+ remoteSlices: [],
+ clientError: undefined,
+ }),
+ { times: 2 },
+ );
+
+ 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();
+
+ procedures.mock("getState", ({ data }) => ({
+ ...(data as Record),
+ libraries,
+ customTypes: generateTypes({ typesCount: 6, libraries }),
+ remoteCustomTypes: [],
+ remoteSlices: [],
+ clientError: undefined,
+ }));
+
+ // 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, procedures }) => {
+ const libraries = generateLibraries({ slicesCount: 1 });
+
+ // We mock a page type with a slice that is a requirement for the review dialog
+ procedures.mock("getState", ({ data }) => ({
+ ...(data as Record),
+ libraries,
+ customTypes: generateTypes({ typesCount: 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..5e2538e570
--- /dev/null
+++ b/playwright/tests/common/sideNav.spec.ts
@@ -0,0 +1,148 @@
+import { expect } from "@playwright/test";
+
+import { test } from "../../fixtures";
+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, procedures }) => {
+ procedures.mock(
+ "versions.getRunningSliceMachineVersion",
+ () => "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, procedures }) => {
+ procedures.mock(
+ "versions.checkIsSliceMachineUpdateAvailable",
+ () => 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, procedures }) => {
+ procedures.mock("versions.checkIsAdapterUpdateAvailable", () => false, {
+ execute: false,
+ });
+
+ procedures.mock(
+ "versions.checkIsSliceMachineUpdateAvailable",
+ () => 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, procedures }) => {
+ const libraries = generateLibraries({ slicesCount: 1 });
+
+ // We mock a page type with a slice that is a requirement for the review dialog
+ procedures.mock("getState", ({ data }) => ({
+ ...(data as Record),
+ libraries,
+ customTypes: generateTypes({ typesCount: 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/utils/MockManagerProcedures.ts b/playwright/utils/MockManagerProcedures.ts
index 9cdca6cec6..bf464bedaf 100644
--- a/playwright/utils/MockManagerProcedures.ts
+++ b/playwright/utils/MockManagerProcedures.ts
@@ -71,10 +71,16 @@ export class MockManagerProcedures {
let existingData: unknown = undefined;
if (procedure.config.execute ?? true) {
- const response = await route.fetch();
- const existingBody = await response.body();
- existingData = (decode(existingBody) as Record<"data", unknown>)
- .data as Record;
+ try {
+ const response = await route.fetch();
+ const existingBody = await response.body();
+ existingData = (decode(existingBody) as Record<"data", unknown>)
+ .data as Record;
+ } catch (_) {
+ // noop: when a test end, it can happen that a route is still pending and
+ // the request is aborted. Without this protection, if a request is
+ // made after the context is closed, the CI will fail.
+ }
}
let newBodyContents: Record = { data: existingData };
diff --git a/yarn.lock b/yarn.lock
index 8c1e60e6bf..045eb66e7e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8329,10 +8329,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