diff --git a/.circleci/config.yml b/.circleci/config.yml index ab1a6f7e..c6df2607 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,13 +3,12 @@ orbs: browser-tools: circleci/browser-tools@1.4.1 jobs: # a collection of steps - build: # runs not using Workflows must have a `build` job as entry point + dusk: # runs not using Workflows must have a `build` job as entry point docker: # run the steps with Docker - #- image: cimg/php:8.0.14-browsers # ...with this image as the primary container; this is where all `steps` will run - image: cimg/php:8.2.4-browsers # ...with this image as the primary container; this is where all `steps` will run auth: username: mydockerhub-user - password: $DOCKERHUB_PASSWORD # context / project UI env-var reference + password: $DOCKERHUB_PASSWORD # context / project UI env-var reference working_directory: ~/laravel # directory where steps will run steps: # a set of executable commands - checkout # special step to check out source code to working directory @@ -45,9 +44,9 @@ jobs: # a collection of steps - run: name: Serve Application background: true - command: php artisan serve + command: php artisan serve - run: php artisan dusk - - run: php artisan test + - run: php artisan test - store_artifacts: path: ./tests/Browser/console destination: console @@ -55,4 +54,28 @@ jobs: # a collection of steps path: ./tests/Browser/screenshots destination: screenshots # See https://circleci.com/docs/2.0/deployment-integrations/ for deploy examples - resource_class: large + resource_class: large + + vue-tests: + docker: + - image: cimg/node:16.5.0 + working_directory: ~/laravel + steps: + - checkout + - restore_cache: + keys: + - node-v1-{{ checksum "package-lock.json" }} + - node-v1- + - run: npm ci + - save_cache: + key: node-v1-{{ checksum "package-lock.json" }} + paths: + - node_modules + - run: npm run test # Assuming you have a "test" script in your package.json for running Vue tests + +workflows: + version: 2 + build-and-test: + jobs: + - dusk + - vue-tests diff --git a/README.md b/README.md index 08bd10fa..6219344b 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Ecommerce site with Laravel 10, Vue 3 and Stripe. - SonarCloud code quality scanner integration on all pull requests -- Laravel tests with CircleCI integration +- Laravel Dusk and Jest tests with CircleCI integration ## Main dependencies: @@ -101,8 +101,6 @@ Ecommerce site with Laravel 10, Vue 3 and Stripe. - Do WCAG analysis and ensure there are no issues -- Add some tests to verify that the cart and checkout works correctly - - Consider adding an admin dashboard - Look into performance optimization diff --git a/tests/Browser/ExampleTest.php b/tests/Browser/ExampleTest.php index 2ce62b62..841ebba9 100644 --- a/tests/Browser/ExampleTest.php +++ b/tests/Browser/ExampleTest.php @@ -16,9 +16,8 @@ public function testBasicExample() { $this->browse(function (Browser $browser) { $browser->visit('/') - //->screenshot('home-page') - ->assertSee('MacBook'); - //->assertPathIs('/'); + ->assertSee('MacBook') + ->assertPathIs('/'); }); } } diff --git a/tests/Vue/Button/button.spec.js b/tests/Vue/Button/button.spec.js index 26f3573a..67d6158f 100644 --- a/tests/Vue/Button/button.spec.js +++ b/tests/Vue/Button/button.spec.js @@ -4,7 +4,6 @@ import BaseButton from "../../../resources/js/components/base/BaseButton.vue"; import "@testing-library/jest-dom"; test("Verify that the button renders and that the default slot works", async () => { - // The render method returns a collection of utilities to query your component. const { getByText } = render(BaseButton, { slots: { default: "Button test" }, }); diff --git a/tests/Vue/LoadingSpinner/loadingspinner.spec.js b/tests/Vue/LoadingSpinner/loadingspinner.spec.js index e2098d5e..121185af 100644 --- a/tests/Vue/LoadingSpinner/loadingspinner.spec.js +++ b/tests/Vue/LoadingSpinner/loadingspinner.spec.js @@ -1 +1,11 @@ -// https://github.com/testing-library/vue-testing-library/tree/main/src/__tests__ +import { render } from "@testing-library/vue"; +import BaseLoadingSpinner from "../../../resources/js/components/base/BaseLoadingSpinner.vue"; + +import "@testing-library/jest-dom"; + +test("Verify that the loading message is rendered", async () => { + const { getByText } = render(BaseLoadingSpinner); + + const loadingMessage = getByText("Loading products ..."); + expect(loadingMessage).toBeInTheDocument(); +}); diff --git a/tests/Vue/Products/ShowallProducts.spec.js b/tests/Vue/Products/ShowallProducts.spec.js new file mode 100644 index 00000000..a4bf43fa --- /dev/null +++ b/tests/Vue/Products/ShowallProducts.spec.js @@ -0,0 +1,102 @@ +import { render } from "@testing-library/vue"; +import ShowAllProducts from "../../../resources/js/components/Products/ShowAllProducts.vue"; +import formatPrice from "../../../resources/js/utils/functions"; + +import "@testing-library/jest-dom"; + +jest.mock("swrv", () => ({ + __esModule: true, + default: jest.fn(), +})); + +const useRouterLink = { + template: "", +}; + +/** + * @description + * Test case to verify that products are rendered correctly with their respective details, + * and that the router-link component works as expected. + * + * The test will: + * 1. Mock the `useSWRV` hook to return a list of products. + * 2. Render the `ShowAllProducts` component. + * 3. Iterate through each product and check if the product's name, price, and image are present in the DOM. + * 4. Check if the list of images found by the role "img" matches the length of the products list. + * + * @async + * @function + * @name verifyProductsAreRenderedAndRouterLinkWorksTest + */ +test("Verify products are rendered and router-link works", async () => { + const useSWRV = require("swrv").default; + const products = [ + { + id: 1, + name: "Product 1", + price: 100, + slug: "product-1", + imageUrl: "image1.jpg", + }, + { + id: 2, + name: "Product 2", + price: 200, + slug: "product-2", + imageUrl: "image2.jpg", + }, + ]; + useSWRV.mockReturnValue({ data: products, error: null }); + + const { getByText, getAllByRole } = render(ShowAllProducts, { + global: { + components: { + "router-link": useRouterLink, + }, + }, + }); + + for (const product of products) { + expect(getByText(product.name)).toBeInTheDocument(); + expect(getByText(formatPrice(product.price))).toBeInTheDocument(); + + const productLink = getByText(product.name).closest("a"); + const productImage = productLink.querySelector("img"); + expect(productImage).toHaveAttribute("src", product.imageUrl); + } + + const images = getAllByRole("img"); + expect(images.length).toBe(products.length); +}); + +/** + * @description + * Test case to verify that an error message is displayed when an error occurs while loading products. + * + * The test will: + * 1. Mock the `useSWRV` hook to return an error. + * 2. Render the `ShowAllProducts` component. + * 3. Check if the error message is present in the DOM. + * + * @async + * @function + * @name verifyErrorMessageIsDisplayedWhenErrorOccursTest + */ +test("Verify error message is displayed when error occurs", async () => { + const useSWRV = require("swrv").default; + useSWRV.mockReturnValue({ + data: null, + error: new Error("Error loading products"), + }); + + const { getByText } = render(ShowAllProducts, { + global: { + components: { + "router-link": useRouterLink, + }, + }, + }); + + const errorMessage = getByText("Error loading products"); + expect(errorMessage).toBeInTheDocument(); +}); diff --git a/tests/Vue/Products/SingleProduct.spec.js b/tests/Vue/Products/SingleProduct.spec.js new file mode 100644 index 00000000..25608114 --- /dev/null +++ b/tests/Vue/Products/SingleProduct.spec.js @@ -0,0 +1,92 @@ +import { fireEvent, render } from "@testing-library/vue"; + +import SingleProduct from "../../../resources/js/components/Products/SingleProduct.vue"; +import { useCart } from "../../../resources/js/store/useCart"; +import formatPrice from "../../../resources/js/utils/functions"; + +import "@testing-library/jest-dom"; + +jest.mock("swrv", () => ({ + __esModule: true, + default: jest.fn(), +})); + +// Mock the useCart function +jest.mock("@/store/useCart", () => { + const addToCartMock = jest.fn(); + return { + useCart: jest.fn(() => ({ addToCart: addToCartMock })), + __addToCartMock: addToCartMock, + }; +}); + +// Mock the useRoute function +jest.mock("vue-router", () => ({ + useRoute: jest.fn(() => ({ + params: { + slug: "product-1", + }, + })), +})); + +const useRouterLink = { + template: "", +}; + +/** + * Test case to verify the 'Add to Cart' functionality. + * + * @name Verify 'Add to Cart' functionality + * @function + * @async + * @memberof module:tests + * + * @description + * The test case does the following: + * 1. Mocks the `useSWRV` hook to return the product data. + * 2. Mocks the `useCart` store to use a Jest function as the `addToCart` method. + * 3. Renders the `SingleProduct` component with the necessary global components. + * 4. Verifies the product information is displayed. + * 5. Clicks the 'Add To Cart' button. + * 6. Verifies that the `addToCart` method in the store is called with the correct product. + * + * @returns {void} + */ +test("Verify 'Add to Cart' functionality", async () => { + const useSWRV = require("swrv").default; + const product = { + id: 1, + name: "Product 1", + price: 100, + slug: "product-1", + imageUrl: "image1.jpg", + description: "Sample description", + }; + useSWRV.mockReturnValue({ data: product, error: null }); + + const addToCartMock = jest.fn(); + useCart.mockReturnValue({ + addToCart: addToCartMock, + }); + + const { getByText } = render(SingleProduct, { + global: { + components: { + "router-link": useRouterLink, + }, + }, + }); + + // Verify product information is displayed + expect(getByText(product.name)).toBeInTheDocument(); + expect(getByText(formatPrice(product.price))).toBeInTheDocument(); + expect(getByText(product.description)).toBeInTheDocument(); + + // Click the 'Add To Cart' button + const addToCartButton = getByText("Add To Cart"); + await fireEvent.click(addToCartButton); + + // Verify that the `addToCart` method in the store is called with the correct product + expect(addToCartMock).toHaveBeenCalledTimes(1); + expect(addToCartMock).toHaveBeenCalledWith({ item: product }); +});