From 57968c0d3cca2d30beb31b3476fdfeb8dc80db3e Mon Sep 17 00:00:00 2001 From: Alican Erdurmaz Date: Tue, 30 May 2023 10:27:08 +0300 Subject: [PATCH] test(examples): add e2e test to supabase and strapi-v4 examples (#4413) * test(examples): add e2e test to supabase and strapi-v4 examples * chore(examples): remove show button * chore(examples): remove show component * test(examples): add show component and test to data-provider-strapi-v4 --- cypress/fixtures/strapi-v4-credentials.json | 4 + cypress/fixtures/supabase-credentials.json | 4 + .../commands/intercepts/api-fake-rest.ts | 121 +++++++ cypress/support/commands/intercepts/index.ts | 123 +------ .../support/commands/intercepts/strapi-v4.ts | 158 +++++++++ .../support/commands/intercepts/supabase.ts | 138 ++++++++ cypress/support/commands/refine/index.ts | 4 + cypress/support/e2e.ts | 2 + cypress/support/index.d.ts | 15 + cypress/support/types/index.ts | 5 + .../data-provider-strapi-v4/cypress.config.ts | 14 + .../cypress/e2e/all.cy.ts | 314 ++++++++++++++++++ examples/data-provider-strapi-v4/package.json | 6 +- examples/data-provider-strapi-v4/src/App.tsx | 48 ++- .../src/interfaces/index.d.ts | 18 + .../src/pages/posts/index.ts | 1 + .../src/pages/posts/list.tsx | 36 +- .../src/pages/posts/show.tsx | 85 +++++ .../data-provider-supabase/cypress.config.ts | 14 + .../cypress/e2e/all.cy.ts | 295 ++++++++++++++++ examples/data-provider-supabase/package.json | 6 +- examples/data-provider-supabase/src/App.tsx | 3 + .../src/pages/posts/list.tsx | 14 +- 23 files changed, 1273 insertions(+), 155 deletions(-) create mode 100644 cypress/fixtures/strapi-v4-credentials.json create mode 100644 cypress/fixtures/supabase-credentials.json create mode 100644 cypress/support/commands/intercepts/api-fake-rest.ts create mode 100644 cypress/support/commands/intercepts/strapi-v4.ts create mode 100644 cypress/support/commands/intercepts/supabase.ts create mode 100644 cypress/support/types/index.ts create mode 100644 examples/data-provider-strapi-v4/cypress.config.ts create mode 100644 examples/data-provider-strapi-v4/cypress/e2e/all.cy.ts create mode 100644 examples/data-provider-strapi-v4/src/pages/posts/show.tsx create mode 100644 examples/data-provider-supabase/cypress.config.ts create mode 100644 examples/data-provider-supabase/cypress/e2e/all.cy.ts diff --git a/cypress/fixtures/strapi-v4-credentials.json b/cypress/fixtures/strapi-v4-credentials.json new file mode 100644 index 000000000000..858385acfea3 --- /dev/null +++ b/cypress/fixtures/strapi-v4-credentials.json @@ -0,0 +1,4 @@ +{ + "email": "demo@refine.dev", + "password": "demodemo" +} diff --git a/cypress/fixtures/supabase-credentials.json b/cypress/fixtures/supabase-credentials.json new file mode 100644 index 000000000000..a8ef280e49c9 --- /dev/null +++ b/cypress/fixtures/supabase-credentials.json @@ -0,0 +1,4 @@ +{ + "email": "info@refine.dev", + "password": "refine-supabase" +} diff --git a/cypress/support/commands/intercepts/api-fake-rest.ts b/cypress/support/commands/intercepts/api-fake-rest.ts new file mode 100644 index 000000000000..7ed418ae106f --- /dev/null +++ b/cypress/support/commands/intercepts/api-fake-rest.ts @@ -0,0 +1,121 @@ +/// +/// + +import { getIdFromURL } from "../../../utils"; + +const hostname = "api.fake-rest.refine.dev"; + +Cypress.Commands.add("interceptGETPosts", () => { + return cy + .intercept( + { + method: "GET", + hostname: hostname, + pathname: "/posts", + }, + { + fixture: "posts.json", + }, + ) + .as("getPosts"); +}); + +Cypress.Commands.add("interceptGETPost", () => { + return cy + .fixture("posts") + .then((posts) => { + return cy.intercept( + { + method: "GET", + hostname: hostname, + pathname: "/posts/*", + }, + + (req) => { + const id = getIdFromURL(req.url); + const post = posts.find((post) => post.id === id); + + if (!post) { + req.reply(404, {}); + return; + } + + req.reply(post); + }, + ); + }) + .as("getPost"); +}); + +Cypress.Commands.add("interceptPOSTPost", () => { + return cy.fixture("posts").then((posts) => + cy + .intercept( + { + method: "POST", + hostname: hostname, + pathname: "/posts", + }, + (req) => { + const merged = Object.assign({}, req.body, { + id: posts.length + 1, + }); + + return req.reply(merged); + }, + ) + .as("postPost"), + ); +}); + +Cypress.Commands.add("interceptPATCHPost", () => { + return cy + .fixture("posts") + .then((posts) => { + return cy.intercept( + { + method: "PATCH", + hostname: hostname, + pathname: "/posts/*", + }, + + (req) => { + const id = getIdFromURL(req.url); + const post = posts.find((post) => post.id === id); + + if (!post) { + return req.reply(404, {}); + } + const merged = Object.assign({}, post, req.body); + return req.reply(merged); + }, + ); + }) + .as("patchPost"); +}); + +Cypress.Commands.add("interceptDELETEPost", () => { + return cy + .intercept( + { + method: "DELETE", + hostname: hostname, + pathname: "/posts/*", + }, + {}, + ) + .as("deletePost"); +}); + +Cypress.Commands.add("interceptGETCategories", () => { + return cy + .intercept( + { + method: "GET", + hostname: hostname, + pathname: "/categories", + }, + { fixture: "categories.json" }, + ) + .as("getCategories"); +}); diff --git a/cypress/support/commands/intercepts/index.ts b/cypress/support/commands/intercepts/index.ts index 26a41d33cda0..9c1fafba248c 100644 --- a/cypress/support/commands/intercepts/index.ts +++ b/cypress/support/commands/intercepts/index.ts @@ -1,119 +1,4 @@ -/// -/// - -import { getIdFromURL } from "../../../utils"; - -Cypress.Commands.add("interceptGETPosts", () => { - return cy - .intercept( - { - method: "GET", - hostname: "api.fake-rest.refine.dev", - pathname: "/posts", - }, - { - fixture: "posts.json", - }, - ) - .as("getPosts"); -}); - -Cypress.Commands.add("interceptGETPost", () => { - return cy - .fixture("posts") - .then((posts) => { - return cy.intercept( - { - method: "GET", - hostname: "api.fake-rest.refine.dev", - pathname: "/posts/*", - }, - - (req) => { - const id = getIdFromURL(req.url); - const post = posts.find((post) => post.id === id); - - if (!post) { - req.reply(404, {}); - return; - } - - req.reply(post); - }, - ); - }) - .as("getPost"); -}); - -Cypress.Commands.add("interceptPOSTPost", () => { - return cy.fixture("posts").then((posts) => - cy - .intercept( - { - method: "POST", - hostname: "api.fake-rest.refine.dev", - pathname: "/posts", - }, - (req) => { - const merged = Object.assign({}, req.body, { - id: posts.length + 1, - }); - - return req.reply(merged); - }, - ) - .as("postPost"), - ); -}); - -Cypress.Commands.add("interceptPATCHPost", () => { - return cy - .fixture("posts") - .then((posts) => { - return cy.intercept( - { - method: "PATCH", - hostname: "api.fake-rest.refine.dev", - pathname: "/posts/*", - }, - - (req) => { - const id = getIdFromURL(req.url); - const post = posts.find((post) => post.id === id); - - if (!post) { - return req.reply(404, {}); - } - const merged = Object.assign({}, post, req.body); - return req.reply(merged); - }, - ); - }) - .as("patchPost"); -}); - -Cypress.Commands.add("interceptDELETEPost", () => { - return cy - .intercept( - { - method: "DELETE", - hostname: "api.fake-rest.refine.dev", - pathname: "/posts/*", - }, - {}, - ) - .as("deletePost"); -}); - -Cypress.Commands.add("interceptGETCategories", () => { - return cy - .intercept( - { - method: "GET", - hostname: "api.fake-rest.refine.dev", - pathname: "/categories", - }, - { fixture: "categories.json" }, - ) - .as("getCategories"); -}); +// add commands to the Cypress chain +import "./api-fake-rest"; +import "./supabase"; +import "./strapi-v4"; diff --git a/cypress/support/commands/intercepts/strapi-v4.ts b/cypress/support/commands/intercepts/strapi-v4.ts new file mode 100644 index 000000000000..51efef899485 --- /dev/null +++ b/cypress/support/commands/intercepts/strapi-v4.ts @@ -0,0 +1,158 @@ +/// +/// + +import { getIdFromURL } from "../../../utils"; + +const hostname = "api.strapi-v4.refine.dev"; +const BASE_PATH = "/api"; + +Cypress.Commands.add("interceptStrapiV4GETPosts", () => { + return cy + .intercept( + { + method: "GET", + hostname: hostname, + pathname: `${BASE_PATH}/posts`, + }, + { + fixture: "posts.json", + }, + ) + .as("strapiV4GetPosts"); +}); + +Cypress.Commands.add("interceptStrapiV4GETPost", () => { + return cy + .fixture("posts") + .then((posts) => { + return cy.intercept( + { + method: "GET", + hostname: hostname, + pathname: `${BASE_PATH}/posts/*`, + }, + + (req) => { + const id = getIdFromURL(req.url); + const post = posts.find( + (post) => post.id.toString() === id.toString(), + ); + + if (!post) { + req.reply(404, {}); + return; + } + + req.reply({ + data: post, + meta: {}, + }); + }, + ); + }) + .as("strapiV4GetPost"); +}); + +Cypress.Commands.add("interceptStrapiV4POSTPost", () => { + return cy.fixture("posts").then((posts) => + cy + .intercept( + { + method: "POST", + hostname: hostname, + pathname: `${BASE_PATH}/posts`, + }, + (req) => { + const merged = Object.assign({}, req.body, { + id: posts.length + 1, + }); + + return req.reply(merged); + }, + ) + .as("strapiV4PostPost"), + ); +}); + +Cypress.Commands.add("interceptStrapiV4PUTPost", () => { + return cy + .fixture("posts") + .then((posts) => { + return cy.intercept( + { + method: "PUT", + hostname: hostname, + pathname: `${BASE_PATH}/posts/*`, + }, + + (req) => { + const id = getIdFromURL(req.url); + const post = posts.find((post) => post.id === id); + + if (!post) { + return req.reply(404, {}); + } + const merged = Object.assign({}, post, req.body); + return req.reply(merged); + }, + ); + }) + .as("strapiV4PutPost"); +}); + +Cypress.Commands.add("interceptStrapiV4DELETEPost", () => { + return cy + .intercept( + { + method: "DELETE", + hostname: hostname, + pathname: `${BASE_PATH}/posts/*`, + }, + {}, + ) + .as("strapiV4DeletePost"); +}); + +Cypress.Commands.add("interceptStrapiV4GETCategories", () => { + return cy + .intercept( + { + method: "GET", + hostname: hostname, + pathname: `${BASE_PATH}/categories`, + }, + { fixture: "categories.json" }, + ) + .as("strapiV4GetCategories"); +}); + +Cypress.Commands.add("interceptStrapiV4GETCategory", () => { + return cy + .fixture("categories") + .then((categories) => { + return cy.intercept( + { + method: "GET", + hostname: hostname, + pathname: `${BASE_PATH}/categories/*`, + }, + + (req) => { + const id = getIdFromURL(req.url); + const category = categories.find( + (category) => category.id.toString() === id.toString(), + ); + + if (!category) { + req.reply(404, {}); + return; + } + + req.reply({ + data: category, + }); + }, + ); + }) + .as("strapiV4GetCategory"); +}); diff --git a/cypress/support/commands/intercepts/supabase.ts b/cypress/support/commands/intercepts/supabase.ts new file mode 100644 index 000000000000..5ac87e8706b3 --- /dev/null +++ b/cypress/support/commands/intercepts/supabase.ts @@ -0,0 +1,138 @@ +/// +/// + +import { ICategory, IPost } from "../../types"; + +const HOSTNAME = "iwdfzvfqbtokqetmbmbp.supabase.co"; +const BASE_PATH = "/rest/v1"; + +const getSupabaseIdFromQuery = (query?: Record) => { + // supabase uses id in query like this {id: 'eq.1'} + return (query?.id as string)?.split(".")?.[1]; +}; + +Cypress.Commands.add("interceptSupabaseGETPosts", () => { + // read posts and categories from fixtures + let posts: (IPost & { + categories: ICategory; + })[] = []; + let categories: ICategory[] = []; + cy.fixture("categories").then((categoriesFixture) => { + categories = categoriesFixture; + }); + // transform fixtures to match supabase response + cy.fixture("posts").then((rawPosts) => { + posts = rawPosts.map((post) => { + // in supabase, the category is not object, but in fixture it is + // because of that, we need to convert it to categoryId + return Object.assign({}, post, { + categoryId: post.category.id, + categories: categories.find( + (category) => category.id === post.category.id, + ), + }); + }); + }); + + return cy + .intercept( + { + method: "GET", + hostname: HOSTNAME, + pathname: `${BASE_PATH}/posts`, + }, + + (req) => { + const id = getSupabaseIdFromQuery(req.query); + if (id) { + const post = posts.find( + (post) => post.id.toString() === id.toString(), + ); + + if (!post) { + return req.reply(404, []); + } + + return req.reply([post]); + } + + return req.reply(posts); + }, + ) + .as("supabaseGetPosts"); +}); + +Cypress.Commands.add("interceptSupabasePOSTPost", () => { + return cy.fixture("posts").then((posts) => + cy + .intercept( + { + method: "POST", + hostname: HOSTNAME, + pathname: `${BASE_PATH}/posts`, + }, + (req) => { + const merged = Object.assign({}, req.body, { + id: posts.length + 1, + }); + + return req.reply(merged); + }, + ) + .as("supabasePostPost"), + ); +}); + +Cypress.Commands.add("interceptSupabasePATCHPost", () => { + return cy + .fixture("posts") + .then((posts) => { + return cy.intercept( + { + method: "PATCH", + hostname: HOSTNAME, + pathname: `${BASE_PATH}/posts`, + }, + + (req) => { + const id = getSupabaseIdFromQuery(req.query); + const post = posts.find( + (post) => post.id.toString() === id.toString(), + ); + + if (!post) { + return req.reply(404, {}); + } + const merged = Object.assign({}, post, req.body); + return req.reply(merged); + }, + ); + }) + .as("supabasePatchPost"); +}); + +Cypress.Commands.add("interceptSupabaseDELETEPost", () => { + return cy + .intercept( + { + method: "DELETE", + hostname: HOSTNAME, + pathname: `${BASE_PATH}/posts`, + }, + {}, + ) + .as("supabaseDeletePost"); +}); + +Cypress.Commands.add("interceptSupabaseGETCategories", () => { + return cy + .intercept( + { + method: "GET", + hostname: HOSTNAME, + pathname: `${BASE_PATH}/categories*`, + }, + { fixture: "categories.json" }, + ) + .as("supabaseGetCategories"); +}); diff --git a/cypress/support/commands/refine/index.ts b/cypress/support/commands/refine/index.ts index 03a4b36d8dd7..86c2987df7d1 100644 --- a/cypress/support/commands/refine/index.ts +++ b/cypress/support/commands/refine/index.ts @@ -14,6 +14,10 @@ export const getEditButton = () => { return cy.get(".refine-edit-button"); }; +export const getShowButton = () => { + return cy.get(".refine-show-button"); +}; + export const getPageHeaderTitle = () => { return cy.get(".refine-pageHeader-title"); }; diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts index f0255131a799..9e5691778bc4 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -33,6 +33,7 @@ import { getEditButton, getPageHeaderTitle, getSaveButton, + getShowButton, } from "./commands/refine"; import { list, create, edit, show } from "./commands/resource"; @@ -59,6 +60,7 @@ Cypress.Commands.add("getSaveButton", getSaveButton); Cypress.Commands.add("getCreateButton", getCreateButton); Cypress.Commands.add("getDeleteButton", getDeleteButton); Cypress.Commands.add("getEditButton", getEditButton); +Cypress.Commands.add("getShowButton", getShowButton); Cypress.Commands.add("getPageHeaderTitle", getPageHeaderTitle); Cypress.Commands.add("getAntdNotification", getAntdNotification); diff --git a/cypress/support/index.d.ts b/cypress/support/index.d.ts index 40997ec45e9e..8f67714539e3 100644 --- a/cypress/support/index.d.ts +++ b/cypress/support/index.d.ts @@ -54,6 +54,7 @@ declare namespace Cypress { getCreateButton(): Chainable>; getDeleteButton(): Chainable>; getEditButton(): Chainable>; + getShowButton(): Chainable>; getPageHeaderTitle(): Chainable>; getAntdNotification(): Chainable>; @@ -108,5 +109,19 @@ declare namespace Cypress { interceptPATCHPost(): Chainable; interceptDELETEPost(): Chainable; interceptGETCategories(): Chainable; + + interceptSupabaseGETPosts(): Chainable; + interceptSupabasePOSTPost(): Chainable; + interceptSupabasePATCHPost(): Chainable; + interceptSupabaseDELETEPost(): Chainable; + interceptSupabaseGETCategories(): Chainable; + + interceptStrapiV4GETPost(): Chainable; + interceptStrapiV4GETPosts(): Chainable; + interceptStrapiV4POSTPost(): Chainable; + interceptStrapiV4PUTPost(): Chainable; + interceptStrapiV4DELETEPost(): Chainable; + interceptStrapiV4GETCategories(): Chainable; + interceptStrapiV4GETCategory(): Chainable; } } diff --git a/cypress/support/types/index.ts b/cypress/support/types/index.ts new file mode 100644 index 000000000000..04bbbfb28b4c --- /dev/null +++ b/cypress/support/types/index.ts @@ -0,0 +1,5 @@ +import posts from "../../fixtures/posts.json"; +import categories from "../../fixtures/categories.json"; + +export type IPost = (typeof posts)[number]; +export type ICategory = (typeof categories)[number]; diff --git a/examples/data-provider-strapi-v4/cypress.config.ts b/examples/data-provider-strapi-v4/cypress.config.ts new file mode 100644 index 000000000000..cf196c8fb6db --- /dev/null +++ b/examples/data-provider-strapi-v4/cypress.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "cypress"; + +export default defineConfig({ + projectId: "sq5j3e", + e2e: { + fixturesFolder: "../../cypress/fixtures", + supportFile: "../../cypress/support/e2e.ts", + }, + chromeWebSecurity: false, + experimentalMemoryManagement: true, + numTestsKeptInMemory: 1, + viewportWidth: 1920, + viewportHeight: 1080, +}); diff --git a/examples/data-provider-strapi-v4/cypress/e2e/all.cy.ts b/examples/data-provider-strapi-v4/cypress/e2e/all.cy.ts new file mode 100644 index 000000000000..201aa615081f --- /dev/null +++ b/examples/data-provider-strapi-v4/cypress/e2e/all.cy.ts @@ -0,0 +1,314 @@ +/// +/// + +describe("data-provider-strapi-v4", () => { + const BASE_URL = "http://localhost:3000"; + + const submitAuthForm = () => { + return cy.get("button[type=submit]").click(); + }; + + const login = () => { + cy.fixture("strapi-v4-credentials").then((auth) => { + cy.get("#email").clear(); + cy.get("#email").type(auth.email); + cy.get("#password").clear(); + cy.get("#password").type(auth.password); + }); + + submitAuthForm(); + }; + + beforeEach(() => { + cy.wait(2000); + cy.clearAllCookies(); + cy.clearAllLocalStorage(); + cy.clearAllSessionStorage(); + + cy.interceptStrapiV4GETPosts(); + cy.interceptStrapiV4GETPost(); + cy.interceptStrapiV4POSTPost(); + cy.interceptStrapiV4PUTPost(); + cy.interceptStrapiV4DELETEPost(); + cy.interceptStrapiV4GETCategories(); + cy.interceptStrapiV4GETCategory(); + + cy.visit(BASE_URL); + }); + + describe("login", () => { + it("should login", () => { + login(); + cy.location("pathname").should("eq", "/posts"); + }); + + it("should throw error if login email is wrong", () => { + cy.get("#email").clear().type("test@test.com"); + cy.get("#password").clear().type("test"); + submitAuthForm(); + cy.getAntdNotification().contains(/invalid/i); + cy.location("pathname").should("eq", "/login"); + }); + + it("should has 'to' param on URL after redirected to /login", () => { + login(); + cy.location("pathname").should("eq", "/posts"); + cy.wait("@strapiV4GetPosts"); + + cy.visit(`${BASE_URL}/test`); + cy.location("pathname").should("eq", "/test"); + cy.clearAllCookies(); + cy.clearAllLocalStorage(); + cy.clearAllSessionStorage(); + cy.reload(); + cy.location("search").should("contains", "to=%2Ftest"); + cy.location("pathname").should("eq", "/login"); + }); + + it("should redirect to /login?to= if user not authenticated", () => { + cy.visit(`${BASE_URL}/test-route`); + cy.get(".ant-card-head-title > .ant-typography").contains( + /sign in to your account/i, + ); + cy.location("search").should("contains", "to=%2Ftest"); + cy.location("pathname").should("eq", "/login"); + }); + }); + + describe("useList", () => { + beforeEach(() => { + login(); + cy.location("pathname").should("eq", "/posts"); + }); + + it("should list with populate", () => { + cy.wait("@strapiV4GetPosts").then(({ request }) => { + const query = request.query; + expect(query.populate).to.deep.equal(["category", "cover"]); + }); + }); + + it("should list with pagination", () => { + cy.wait("@strapiV4GetPosts"); + cy.getAntdLoadingOverlay().should("not.exist"); + + cy.wait("@strapiV4GetPosts").then(({ request }) => { + const query = request.query; + expect(query.pagination).to.deep.equal({ + page: "1", + pageSize: "10", + }); + }); + + cy.get(".ant-pagination-item-2 > a").click(); + cy.wait("@strapiV4GetPosts").then(({ request }) => { + const query = request.query; + expect(query.pagination).to.deep.equal({ + page: "2", + pageSize: "10", + }); + }); + }); + + it("should sort", () => { + cy.wait("@strapiV4GetPosts"); + cy.getAntdLoadingOverlay().should("not.exist"); + + cy.wait("@strapiV4GetPosts").then(({ request }) => { + const query = request.query; + expect(query.sort).to.eq("id:desc"); + }); + + cy.getAntdColumnSorter(0).click(); + cy.wait("@strapiV4GetPosts").then(({ request }) => { + const query = request.query; + expect(query?.sort); + }); + + cy.getAntdColumnSorter(0).click(); + cy.wait("@strapiV4GetPosts").then(({ request }) => { + const query = request.query; + expect(query.sort).to.eq("id:asc"); + }); + }); + + it("should filter", () => { + cy.wait("@strapiV4GetCategories"); + cy.wait("@strapiV4GetPosts"); + cy.wait("@strapiV4GetPosts"); + cy.getAntdLoadingOverlay().should("not.exist"); + + cy.getAntdFilterTrigger(0).click(); + cy.get(".ant-select-selector").eq(1).click(); + cy.fixture("categories").then((categories) => { + const category = categories[0]; + + cy.get(".ant-select-item-option-content") + .contains(category.title) + .click(); + cy.contains(/filter/i).click({ force: true }); + + cy.wait("@strapiV4GetPosts").then(({ request }) => { + const query = request.query; + console.log(query); + expect(query?.["filters["]).to.deep.equal({ + category: { + id: { + $in: ["1"], + }, + }, + }); + }); + }); + }); + }); + + describe("form", () => { + const mockPost = { + title: "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", + content: + "Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.", + status: "Published", + }; + + beforeEach(() => { + login(); + cy.location("pathname").should("eq", "/posts"); + cy.wait("@strapiV4GetPosts"); + cy.getAntdLoadingOverlay().should("not.exist"); + }); + + it("should show", () => { + cy.getShowButton().first().click(); + + cy.location("pathname").should("include", "/posts/show/"); + cy.getAntdLoadingOverlay().should("not.exist"); + + cy.wait("@strapiV4GetPost").then(({ request, response }) => { + expect(request.query).to.deep.equal({ + populate: ["category", "cover"], + }); + expect(response?.body).to.deep.equal({ + data: { + id: 1, + title: "Ut Voluptatem Est", + content: + "Repellendus temporibus provident nobis. Non adipisci quod et est dolorem sed qui. A ut omnis. Et perspiciatis quibusdam maiores aliquid est fugit nam odit. Aut aliquam consectetur deleniti commodi velit. Eum eum aperiam voluptate quos quo. Ut quia doloribus a. Molestiae non est fugit enim fugiat non ea quas accusamus. Consequuntur voluptatem nesciunt dolorum expedita optio deserunt. Illo dolorem et similique.", + category: { + id: 1, + }, + status: "published", + createdAt: "2022-06-12T11:03:09.829Z", + slug: "ut-voluptatem-est", + }, + meta: {}, + }); + }); + + cy.wait("@strapiV4GetCategory").then(({ request, response }) => { + expect(request.url).to.includes("/categories/1"); + expect(response?.body).to.deep.equal({ + data: { + id: 1, + title: "Sint Ipsam Tempora", + }, + }); + }); + }); + + it("should create", () => { + cy.getCreateButton().click(); + cy.location("pathname").should("eq", "/posts/create"); + + cy.get("#title").clear(); + cy.get("#title").type(mockPost.title); + cy.get("#content textarea").clear(); + cy.get("#content textarea").type(mockPost.content); + cy.setAntdDropdown({ id: "category", selectIndex: 0 }); + cy.getSaveButton().click(); + + cy.wait("@strapiV4PostPost").then(({ request }) => { + expect(request.body).to.deep.equal({ + data: { + title: mockPost.title, + content: mockPost.content, + category: 1, + }, + }); + cy.location("pathname").should("eq", "/posts"); + cy.getAntdNotification().contains(/success/i); + }); + }); + + it("should edit", () => { + cy.getEditButton().first().click(); + + cy.wait("@strapiV4GetPost"); + cy.location("pathname").should("include", "/posts/edit/"); + cy.getAntdLoadingOverlay().should("not.exist"); + cy.getSaveButton().should("not.be.disabled"); + + cy.get("#title").clear(); + cy.get("#title").type(mockPost.title); + cy.get("#content textarea").clear(); + cy.get("#content textarea").type(mockPost.content); + cy.setAntdDropdown({ id: "category_id", selectIndex: 0 }); + cy.getSaveButton().click(); + + cy.wait("@strapiV4PutPost").then(({ request }) => { + expect(request.body).to.deep.equal({ + data: { + title: mockPost.title, + content: mockPost.content, + category: { + id: 1, + }, + }, + }); + + cy.location("pathname").should("eq", "/posts"); + cy.getAntdNotification().contains(/success/i); + }); + }); + + it("should delete", () => { + cy.getEditButton().first().click(); + + cy.wait("@strapiV4GetPost"); + cy.location("pathname").should("include", "/posts/edit/"); + cy.getAntdLoadingOverlay().should("not.exist"); + cy.getSaveButton().should("not.be.disabled"); + + cy.getDeleteButton().click(); + cy.getAntdPopoverDeleteButton().click(); + cy.wait("@strapiV4DeletePost").then(() => { + cy.location("pathname").should("eq", "/posts"); + cy.getAntdNotification().contains(/success/i); + }); + }); + }); + + describe("change locale", () => { + beforeEach(() => { + login(); + cy.location("pathname").should("eq", "/posts"); + cy.wait("@strapiV4GetPosts"); + cy.getAntdLoadingOverlay().should("not.exist"); + }); + + it("should change locale", () => { + cy.wait("@strapiV4GetPosts"); + cy.get("#locale").contains("Deutsch").click(); + cy.wait("@strapiV4GetPosts").then(({ request }) => { + const query = request.query; + expect(query.locale).to.eq("de"); + }); + + cy.get("#locale").contains("English").click(); + cy.wait("@strapiV4GetPosts").then(({ request }) => { + const query = request.query; + expect(query.locale).to.eq("en"); + }); + }); + }); +}); diff --git a/examples/data-provider-strapi-v4/package.json b/examples/data-provider-strapi-v4/package.json index eae0c696ef40..70b1c83a5fe0 100644 --- a/examples/data-provider-strapi-v4/package.json +++ b/examples/data-provider-strapi-v4/package.json @@ -26,7 +26,8 @@ "build": "cross-env DISABLE_ESLINT_PLUGIN=true refine build", "test": "react-scripts test", "eject": "react-scripts eject", - "refine": "refine" + "refine": "refine", + "cypress": "cypress open -C ./cypress.config.ts" }, "browserslist": { "production": [ @@ -42,6 +43,7 @@ }, "devDependencies": { "@babel/core": "^7.13.14", - "http-proxy-middleware": "^2.0.6" + "http-proxy-middleware": "^2.0.6", + "cypress": "^12.11.0" } } diff --git a/examples/data-provider-strapi-v4/src/App.tsx b/examples/data-provider-strapi-v4/src/App.tsx index 2c570e798e5e..dbe302650622 100644 --- a/examples/data-provider-strapi-v4/src/App.tsx +++ b/examples/data-provider-strapi-v4/src/App.tsx @@ -22,7 +22,7 @@ import { BrowserRouter, Routes, Route, Outlet } from "react-router-dom"; import "@refinedev/antd/dist/reset.css"; -import { PostList, PostCreate, PostEdit } from "pages/posts"; +import { PostList, PostCreate, PostEdit, PostShow } from "pages/posts"; import { UserList } from "pages/users"; import { CategoryList, CategoryCreate, CategoryEdit } from "pages/categories"; @@ -35,23 +35,36 @@ const App: React.FC = () => { const authProvider: AuthBindings = { login: async ({ email, password }) => { - const { data, status } = await strapiAuthHelper.login( - email, - password, - ); - if (status === 200) { - localStorage.setItem(TOKEN_KEY, data.jwt); - - // set header axios instance - axiosInstance.defaults.headers.common[ - "Authorization" - ] = `Bearer ${data.jwt}`; - + try { + const { data, status } = await strapiAuthHelper.login( + email, + password, + ); + if (status === 200) { + localStorage.setItem(TOKEN_KEY, data.jwt); + + // set header axios instance + axiosInstance.defaults.headers.common[ + "Authorization" + ] = `Bearer ${data.jwt}`; + + return { + success: true, + redirectTo: "/", + }; + } + } catch (error: any) { + const errorObj = + error?.response?.data?.message?.[0]?.messages?.[0]; return { - success: true, - redirectTo: "/", + success: false, + error: { + message: errorObj?.mesage || "Login failed", + name: errorObj?.id || "Invalid email or password", + }, }; } + return { success: false, error: { @@ -127,6 +140,10 @@ const App: React.FC = () => { list: "/posts", create: "/posts/create", edit: "/posts/edit/:id", + show: "/posts/show/:id", + meta: { + canDelete: true, + }, }, { name: "categories", @@ -168,6 +185,7 @@ const App: React.FC = () => { } /> } /> } /> + } /> diff --git a/examples/data-provider-strapi-v4/src/interfaces/index.d.ts b/examples/data-provider-strapi-v4/src/interfaces/index.d.ts index d7b07af4679d..eab637cda0c3 100644 --- a/examples/data-provider-strapi-v4/src/interfaces/index.d.ts +++ b/examples/data-provider-strapi-v4/src/interfaces/index.d.ts @@ -10,4 +10,22 @@ export interface IPost { content: string; locale: string; createdAt: string; + cover: { + id: number; + name: string; + alternativeText: any; + caption: any; + width: number; + height: number; + hash: string; + ext: string; + mime: string; + size: number; + url: string; + previewUrl: any; + provider: string; + provider_metadata: any; + createdAt: string; + updatedAt: string; + }[]; } diff --git a/examples/data-provider-strapi-v4/src/pages/posts/index.ts b/examples/data-provider-strapi-v4/src/pages/posts/index.ts index b9af745e6bcf..9da022ffe482 100644 --- a/examples/data-provider-strapi-v4/src/pages/posts/index.ts +++ b/examples/data-provider-strapi-v4/src/pages/posts/index.ts @@ -1,3 +1,4 @@ export * from "./list"; export * from "./create"; export * from "./edit"; +export * from "./show"; diff --git a/examples/data-provider-strapi-v4/src/pages/posts/list.tsx b/examples/data-provider-strapi-v4/src/pages/posts/list.tsx index a31fe28b7af3..15a1a89b2a54 100644 --- a/examples/data-provider-strapi-v4/src/pages/posts/list.tsx +++ b/examples/data-provider-strapi-v4/src/pages/posts/list.tsx @@ -11,6 +11,7 @@ import { EditButton, DeleteButton, ImageField, + ShowButton, } from "@refinedev/antd"; import { Table, Select, Space, Form, Radio, Tag } from "antd"; @@ -147,20 +148,27 @@ export const PostList: React.FC = () => { title="Actions" dataIndex="actions" - render={(_, record) => ( - - - - - )} + render={(_, record) => { + return ( + + + + + + ); + }} /> diff --git a/examples/data-provider-strapi-v4/src/pages/posts/show.tsx b/examples/data-provider-strapi-v4/src/pages/posts/show.tsx new file mode 100644 index 000000000000..c56a0744e92c --- /dev/null +++ b/examples/data-provider-strapi-v4/src/pages/posts/show.tsx @@ -0,0 +1,85 @@ +import { useShow, IResourceComponentsProps, useOne } from "@refinedev/core"; + +import { + Show, + MarkdownField, + ListButton, + EditButton, + RefreshButton, + ImageField, +} from "@refinedev/antd"; + +import { Space, Typography } from "antd"; + +import { IPost, ICategory } from "interfaces"; +import { API_URL } from "../../constants"; + +const { Title, Text } = Typography; + +export const PostShow: React.FC = () => { + const { queryResult } = useShow({ + metaData: { populate: ["category", "cover"] }, + }); + + const { data, isLoading } = queryResult; + const record = data?.data; + + const { data: categoryData, isLoading: categoryIsLoading } = + useOne({ + resource: "categories", + id: record?.category?.id || "", + queryOptions: { + enabled: !!record, + }, + }); + + const handleRefresh = () => { + queryResult.refetch(); + }; + + return ( + + + + + + ), + }} + > + Id + {record?.id} + + Title + {record?.title} + + Category + + {categoryIsLoading ? "Loading..." : categoryData?.data?.title} + + + Content + + + Images + + {record?.cover ? ( + record?.cover.map((attributes) => { + return ( + + ); + }) + ) : ( + Not found any images + )} + + + ); +}; diff --git a/examples/data-provider-supabase/cypress.config.ts b/examples/data-provider-supabase/cypress.config.ts new file mode 100644 index 000000000000..cf196c8fb6db --- /dev/null +++ b/examples/data-provider-supabase/cypress.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "cypress"; + +export default defineConfig({ + projectId: "sq5j3e", + e2e: { + fixturesFolder: "../../cypress/fixtures", + supportFile: "../../cypress/support/e2e.ts", + }, + chromeWebSecurity: false, + experimentalMemoryManagement: true, + numTestsKeptInMemory: 1, + viewportWidth: 1920, + viewportHeight: 1080, +}); diff --git a/examples/data-provider-supabase/cypress/e2e/all.cy.ts b/examples/data-provider-supabase/cypress/e2e/all.cy.ts new file mode 100644 index 000000000000..e9b7a412f5cd --- /dev/null +++ b/examples/data-provider-supabase/cypress/e2e/all.cy.ts @@ -0,0 +1,295 @@ +/// +/// + +describe("data-provider-supabase", () => { + const BASE_URL = "http://localhost:3000"; + + const submitAuthForm = () => { + return cy.get("button[type=submit]").click(); + }; + + const login = () => { + cy.fixture("supabase-credentials").then((auth) => { + cy.get("#email").clear(); + cy.get("#email").type(auth.email); + cy.get("#password").clear(); + cy.get("#password").type(auth.password); + }); + + submitAuthForm(); + }; + + beforeEach(() => { + cy.clearAllCookies(); + cy.clearAllLocalStorage(); + cy.clearAllSessionStorage(); + + cy.interceptSupabaseGETPosts(); + cy.interceptSupabasePOSTPost(); + cy.interceptSupabasePATCHPost(); + cy.interceptSupabaseDELETEPost(); + cy.interceptSupabaseGETCategories(); + + cy.visit(BASE_URL); + }); + + describe("login", () => { + it("should login", () => { + login(); + cy.location("pathname").should("eq", "/posts"); + }); + + it("should throw error if login email is wrong", () => { + cy.get("#email").clear().type("test@test.com"); + cy.get("#password").clear().type("test"); + submitAuthForm(); + cy.getAntdNotification().contains(/invalid/i); + cy.location("pathname").should("eq", "/login"); + }); + + it("should has 'to' param on URL after redirected to /login", () => { + login(); + cy.location("pathname").should("eq", "/posts"); + cy.wait("@supabaseGetPosts"); + + cy.visit(`${BASE_URL}/test`); + cy.location("pathname").should("eq", "/test"); + cy.clearAllCookies(); + cy.clearAllLocalStorage(); + cy.clearAllSessionStorage(); + cy.reload(); + cy.location("search").should("contains", "to=%2Ftest"); + cy.location("pathname").should("eq", "/login"); + }); + + it("should redirect to /login?to= if user not authenticated", () => { + cy.visit(`${BASE_URL}/test-route`); + cy.get(".ant-card-head-title > .ant-typography").contains( + /sign in to your account/i, + ); + cy.location("search").should("contains", "to=%2Ftest"); + cy.location("pathname").should("eq", "/login"); + }); + }); + + describe("useList", () => { + beforeEach(() => { + login(); + cy.location("pathname").should("eq", "/posts"); + }); + + it("should list with select", () => { + cy.wait("@supabaseGetPosts").then(({ request }) => { + const query = request.query; + expect(query).to.deep.equal({ + select: "*,categories(title)", + offset: "0", + limit: "10", + order: "id.asc", + }); + }); + }); + + it("should list with pagination", () => { + cy.wait("@supabaseGetPosts"); + cy.getAntdLoadingOverlay().should("not.exist"); + + cy.get(".ant-pagination-item-2 > a").click(); + cy.wait("@supabaseGetPosts"); + cy.wait("@supabaseGetPosts").then(({ request }) => { + const query = request.query; + expect(query).to.deep.equal({ + select: "*,categories(title)", + offset: "10", + limit: "10", + order: "id.asc", + }); + }); + }); + + it("should sort", () => { + cy.wait("@supabaseGetPosts"); + cy.getAntdLoadingOverlay().should("not.exist"); + + cy.wait("@supabaseGetPosts").then(({ request }) => { + const query = request.query; + expect(query).to.deep.equal({ + select: "*,categories(title)", + offset: "0", + limit: "10", + order: "id.asc", + }); + }); + + cy.getAntdColumnSorter(0).click(); + cy.wait("@supabaseGetPosts").then(({ request }) => { + const query = request.query; + expect(query).to.deep.equal({ + select: "*,categories(title)", + offset: "0", + limit: "10", + order: "id.desc", + }); + }); + + cy.getAntdColumnSorter(0).click(); + cy.wait("@supabaseGetPosts").then(({ request }) => { + const query = request.query; + expect(query).to.deep.equal({ + select: "*,categories(title)", + offset: "0", + limit: "10", + }); + }); + }); + + it("should filter", () => { + cy.wait("@supabaseGetCategories"); + cy.wait("@supabaseGetPosts"); + cy.getAntdLoadingOverlay().should("not.exist"); + + cy.getAntdFilterTrigger(0).click(); + cy.get(".ant-select-selector").click(); + cy.fixture("categories").then((categories) => { + const category = categories[0]; + + cy.get(".ant-select-item-option-content") + .contains(category.title) + .click(); + cy.contains(/filter/i).click({ force: true }); + + cy.wait("@supabaseGetPosts"); + cy.wait("@supabaseGetPosts").then(({ request }) => { + const query = request.query; + expect(query).to.deep.equal({ + select: "*,categories(title)", + offset: "0", + limit: "10", + order: "id.asc", + categoryId: `in.(${category.id})`, + }); + }); + }); + }); + }); + + describe("useShow", () => { + beforeEach(() => { + login(); + cy.location("pathname").should("eq", "/posts"); + }); + + it("should show", () => { + cy.wait("@supabaseGetPosts"); + cy.wait("@supabaseGetPosts"); + + cy.getShowButton().first().click(); + cy.location("pathname").should("contains", "/posts/show/"); + cy.wait("@supabaseGetPosts").then(({ request, response }) => { + const query = request.query; + const body = response?.body?.[0]; + + expect(query).to.deep.equal({ + select: "*", + id: "eq.1", + }); + + cy.getAntdLoadingOverlay().should("not.exist"); + console.log(body); + cy.contains(body.title); + cy.contains(body.content); + cy.contains(body.categories?.title); + }); + }); + }); + + describe("form", () => { + const mockPost = { + title: "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", + content: + "Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.", + status: "Published", + }; + + const fillForm = () => { + cy.get("#title").clear(); + cy.get("#title").type(mockPost.title); + cy.get("#content textarea").clear(); + cy.get("#content textarea").type(mockPost.content); + cy.setAntdDropdown({ id: "categoryId", selectIndex: 0 }); + }; + + beforeEach(() => { + login(); + cy.location("pathname").should("eq", "/posts"); + }); + + it("should create", () => { + cy.wait("@supabaseGetPosts"); + cy.getCreateButton().click(); + cy.location("pathname").should("eq", "/posts/create"); + + fillForm(); + cy.getSaveButton().click(); + + cy.wait("@supabasePostPost").then(({ request, response }) => { + expect(request.body).to.deep.equal({ + title: mockPost.title, + content: mockPost.content, + categoryId: 1, + }); + expect(response?.body?.title).to.eq(mockPost.title); + expect(response?.body?.content).to.eq(mockPost.content); + expect(response?.body?.categoryId).to.eq(1); + + cy.location("pathname").should("eq", "/posts"); + cy.getAntdNotification().contains(/success/i); + }); + }); + + it("should edit", () => { + cy.wait("@supabaseGetPosts"); + cy.getEditButton().first().click(); + cy.wait("@supabaseGetPosts"); + cy.location("pathname").should("include", "/posts/edit/"); + cy.getAntdLoadingOverlay().should("not.exist"); + cy.getSaveButton().should("not.be.disabled"); + + fillForm(); + cy.getSaveButton().click(); + + cy.wait("@supabasePatchPost").then(({ request, response }) => { + expect(request.body).to.deep.equal({ + title: mockPost.title, + content: mockPost.content, + categoryId: 1, + }); + expect(response?.body?.title).to.eq(mockPost.title); + expect(response?.body?.content).to.eq(mockPost.content); + expect(response?.body?.categoryId).to.eq(1); + + cy.location("pathname").should("eq", "/posts"); + cy.getAntdNotification().contains(/success/i); + }); + }); + + it("should delete", () => { + cy.wait("@supabaseGetPosts"); + cy.getEditButton().first().click(); + cy.wait("@supabaseGetPosts"); + cy.location("pathname").should("include", "/posts/edit/"); + cy.getAntdLoadingOverlay().should("not.exist"); + cy.getSaveButton().should("not.be.disabled"); + + cy.getDeleteButton().click(); + cy.getAntdPopoverDeleteButton().click(); + cy.wait("@supabaseDeletePost").then(({ request }) => { + expect(request.query).to.deep.equal({ + id: "eq.1", + }); + cy.location("pathname").should("eq", "/posts"); + cy.getAntdNotification().contains(/success/i); + }); + }); + }); +}); diff --git a/examples/data-provider-supabase/package.json b/examples/data-provider-supabase/package.json index ad1406214042..d60f49d01ebe 100644 --- a/examples/data-provider-supabase/package.json +++ b/examples/data-provider-supabase/package.json @@ -20,11 +20,15 @@ "typescript": "^4.7.4", "antd": "^5.0.5" }, + "devDependencies": { + "cypress": "^12.11.0" + }, "scripts": { "start": "cross-env DISABLE_ESLINT_PLUGIN=true refine start", "build": "cross-env DISABLE_ESLINT_PLUGIN=true refine build", "eject": "react-scripts eject", - "refine": "refine" + "refine": "refine", + "cypress": "cypress open -C ./cypress.config.ts" }, "browserslist": { "production": [ diff --git a/examples/data-provider-supabase/src/App.tsx b/examples/data-provider-supabase/src/App.tsx index a10e79052a59..d19137be9644 100644 --- a/examples/data-provider-supabase/src/App.tsx +++ b/examples/data-provider-supabase/src/App.tsx @@ -282,6 +282,9 @@ const App: React.FC = () => { create: "/posts/create", edit: "/posts/edit/:id", show: "/posts/show/:id", + meta: { + canDelete: true, + }, }, ]} notificationProvider={notificationProvider} diff --git a/examples/data-provider-supabase/src/pages/posts/list.tsx b/examples/data-provider-supabase/src/pages/posts/list.tsx index edcd833c8340..31c6719eccca 100644 --- a/examples/data-provider-supabase/src/pages/posts/list.tsx +++ b/examples/data-provider-supabase/src/pages/posts/list.tsx @@ -1,4 +1,4 @@ -import { IResourceComponentsProps } from "@refinedev/core"; +import { IResourceComponentsProps, getDefaultFilter } from "@refinedev/core"; import { List, @@ -15,7 +15,7 @@ import { Table, Space, Select } from "antd"; import { IPost, ICategory } from "interfaces"; export const PostList: React.FC = () => { - const { tableProps, sorter } = useTable({ + const { tableProps, sorters, filters } = useTable({ initialSorter: [ { field: "id", @@ -39,13 +39,14 @@ export const PostList: React.FC = () => { dataIndex="id" title="ID" sorter - defaultSortOrder={getDefaultSortOrder("id", sorter)} + defaultSortOrder={getDefaultSortOrder("id", sorters)} /> = () => { title="Category" defaultSortOrder={getDefaultSortOrder( "categories.title", - sorter, + sorters, + )} + defaultFilteredValue={getDefaultFilter( + "categoryId", + filters, + "in", )} filterDropdown={(props) => (