diff --git a/.github/actions/restore-cache/action.yml b/.github/actions/restore-cache/action.yml
index 90c32680ce..dee4947726 100644
--- a/.github/actions/restore-cache/action.yml
+++ b/.github/actions/restore-cache/action.yml
@@ -10,8 +10,6 @@ runs:
path: |
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 +19,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 114849bb8b..36feb1ff2e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -165,12 +165,16 @@ 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
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/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_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 571b9e651f..f324ad156f 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,28 +63,53 @@ 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 user not onboarded:
+
+```ts
+test.run({ onboarded: false })("I can ...",
+ async ({ sliceBuilderPage, slicesListPage }) => {
+ // Test content
+ });
+```
-Example for a logged in user not onboarded:
+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 +127,7 @@ await mockManagerProcedures({
path: "getState",
data: (data) => ({
...data,
- libraries: emptyLibraries,
+ libraries: generateLibraries({ nbSlices: 0 }),
customTypes: [],
remoteCustomTypes: [],
remoteSlices: [],
@@ -123,7 +141,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 +230,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 d3f0a1e9b9..49baa7e178 100644
--- a/playwright/package.json
+++ b/playwright/package.json
@@ -5,22 +5,22 @@
"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"
+ "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 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/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/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/playwright.config.ts b/playwright/playwright.config.ts
index f9d6f0037e..6dc5651d0b 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,6 +22,9 @@ const config = {
},
],
+ // Retry on CI only.
+ retries: process.env["CI"] ? 2 : 0,
+
// Reporter to use.
reporter: process.env["CI"]
? [["github"], ["html"]]
@@ -65,8 +65,8 @@ const config = {
webServer: [
{
cwd: "..",
- command: "yarn dev",
- url: `http://localhost:3000/`,
+ command: process.env["CI"] ? "yarn dev:e2e-next" : "yarn dev",
+ url: "http://localhost:8000/",
reuseExistingServer: !process.env["CI"],
stdout: "pipe",
timeout: 120_000,
@@ -74,16 +74,12 @@ 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,
} satisfies PlaywrightTestConfig;
export default config;
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/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 2518cfbf9b..7a029576b4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8628,10 +8628,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