diff --git a/.github/scripts/medusa-config.js b/.github/scripts/medusa-config.js new file mode 100644 index 000000000..5a2c69042 --- /dev/null +++ b/.github/scripts/medusa-config.js @@ -0,0 +1,117 @@ +const dotenv = require("dotenv"); + +let ENV_FILE_NAME = ""; +switch (process.env.NODE_ENV) { + case "production": + ENV_FILE_NAME = ".env.production"; + break; + case "staging": + ENV_FILE_NAME = ".env.staging"; + break; + case "test": + ENV_FILE_NAME = ".env.test"; + break; + case "development": + default: + ENV_FILE_NAME = ".env"; + break; +} + +try { + dotenv.config({ path: process.cwd() + "/" + ENV_FILE_NAME }); +} catch (e) {} + +// CORS when consuming Medusa from admin +const ADMIN_CORS = + process.env.ADMIN_CORS || "http://localhost:7000,http://localhost:7001"; + +// CORS to avoid issues when consuming Medusa from a client +const STORE_CORS = process.env.STORE_CORS || "http://localhost:8000"; + +const DATABASE_URL = + process.env.DATABASE_URL || "postgres://medusa:password@localhost/medusa"; + +const REDIS_URL = process.env.REDIS_URL || "redis://localhost:6379"; + +const plugins = [ + `medusa-fulfillment-manual`, + `medusa-payment-manual`, + { + resolve: `@medusajs/file-local`, + options: { + upload_dir: "uploads", + }, + }, + { + resolve: "@medusajs/admin", + /** @type {import('@medusajs/admin').PluginOptions} */ + options: { + autoRebuild: true, + develop: { + open: process.env.OPEN_BROWSER !== "false", + }, + }, + }, + { + resolve: `medusa-plugin-meilisearch`, + options: { + config: { + host: process.env.MEILISEARCH_HOST, + apiKey: process.env.MEILISEARCH_API_KEY, + }, + settings: { + products: { + indexSettings: { + searchableAttributes: [ + "title", + "description", + "variant_sku", + ], + displayedAttributes: [ + "id", + "title", + "description", + "variant_sku", + "thumbnail", + "handle", + ], + }, + primaryKey: "id", + }, + }, + }, + }, +]; + +const modules = { + /*eventBus: { + resolve: "@medusajs/event-bus-redis", + options: { + redisUrl: REDIS_URL + } + }, + cacheService: { + resolve: "@medusajs/cache-redis", + options: { + redisUrl: REDIS_URL + } + },*/ +}; + +/** @type {import('@medusajs/medusa').ConfigModule["projectConfig"]} */ +const projectConfig = { + jwtSecret: process.env.JWT_SECRET, + cookieSecret: process.env.COOKIE_SECRET, + store_cors: STORE_CORS, + database_url: DATABASE_URL, + admin_cors: ADMIN_CORS, + // Uncomment the following lines to enable REDIS + redis_url: REDIS_URL +}; + +/** @type {import('@medusajs/medusa').ConfigModule} */ +module.exports = { + projectConfig, + plugins, + modules, +}; diff --git a/.github/workflows/test-e2e.yaml b/.github/workflows/test-e2e.yaml index 136a748cd..789445114 100644 --- a/.github/workflows/test-e2e.yaml +++ b/.github/workflows/test-e2e.yaml @@ -29,7 +29,16 @@ env: DATABASE_TYPE: "postgres" REDIS_URL: redis://localhost:6379 DATABASE_URL: postgres://test_medusa_user:password@localhost/test_medusa_db + MEILISEARCH_HOST: http://localhost:7700 + MEILISEARCH_API_KEY: meili_api_key + NEXT_PUBLIC_BASE_URL: http://localhost:8000 + NEXT_PUBLIC_DEFAULT_REGION: us + NEXT_PUBLIC_MEDUSA_BACKEND_URL: http://localhost:9000 + NEXT_PUBLIC_INDEX_NAME: products + NEXT_PUBLIC_SEARCH_ENDPOINT: http://127.0.0.1:7700 + NEXT_PUBLIC_SEARCH_API_KEY: meili_api_key + REVALIDATE_SECRET: supersecret jobs: e2e-test-runner: @@ -53,6 +62,9 @@ jobs: meilisearch: image: getmeili/meilisearch:v1.7 + env: + MEILI_MASTER_KEY: meili_api_key + MEILI_ENV: development ports: - 7700:7700 options: >- @@ -100,11 +112,18 @@ jobs: --db-database ${{ env.TEST_POSTGRES_DATABASE }} \ --db-host ${{ env.TEST_POSTGRES_HOST }} \ --db-port ${{ env.TEST_POSTGREST_PORT }} - + - name: Build the backend working-directory: ../backend run: yarn build:admin + - name: Setup search in the backend + working-directory: ../backend + run: yarn add medusa-plugin-meilisearch + + - name: Move custom medusa config to the backend + run: cp .github/scripts/medusa-config.js ../backend/medusa-config.js + - name: Seed data from default seed file working-directory: ../backend run: medusa seed --seed-file=data/seed.json @@ -119,9 +138,6 @@ jobs: - name: Install playwright run: yarn playwright install --with-deps - - name: Copy environment - run: cp .env.template .env - - name: Setup frontend run: yarn build diff --git a/e2e/fixtures/base/base-page.ts b/e2e/fixtures/base/base-page.ts index 4aa7cc91e..831eab97f 100644 --- a/e2e/fixtures/base/base-page.ts +++ b/e2e/fixtures/base/base-page.ts @@ -1,11 +1,13 @@ import { CartDropdown } from "./cart-dropdown" import { NavMenu } from "./nav-menu" import { Page, Locator } from "@playwright/test" +import { SearchModal } from "./search-modal" export class BasePage { page: Page navMenu: NavMenu cartDropdown: CartDropdown + searchModal: SearchModal accountLink: Locator searchLink: Locator storeLink: Locator @@ -15,6 +17,7 @@ export class BasePage { this.page = page this.navMenu = new NavMenu(page) this.cartDropdown = new CartDropdown(page) + this.searchModal = new SearchModal(page) this.accountLink = page.getByTestId("nav-account-link") this.storeLink = page.getByTestId("nav-store-link") this.searchLink = page.getByTestId("nav-search-link") diff --git a/e2e/fixtures/base/search-modal.ts b/e2e/fixtures/base/search-modal.ts new file mode 100644 index 000000000..2724dcb4c --- /dev/null +++ b/e2e/fixtures/base/search-modal.ts @@ -0,0 +1,36 @@ +import { Page, Locator } from "@playwright/test" +import { BaseModal } from "./base-modal" +import { NavMenu } from "./nav-menu" + +export class SearchModal extends BaseModal { + searchInput: Locator + searchResults: Locator + noSearchResultsContainer: Locator + searchResult: Locator + searchResultTitle: Locator + + constructor(page: Page) { + super(page, page.getByTestId("search-modal-container")) + this.searchInput = this.container.getByTestId("search-input") + this.searchResults = this.container.getByTestId("search-results") + this.noSearchResultsContainer = this.container.getByTestId( + "no-search-results-container" + ) + this.searchResult = this.container.getByTestId("search-result") + this.searchResultTitle = this.container.getByTestId("search-result-title") + } + + async open() { + const menu = new NavMenu(this.page) + await menu.open() + await menu.searchLink.click() + await this.container.waitFor({ state: "visible" }) + } + + async close() { + const viewport = this.page.viewportSize() + const y = viewport ? viewport.height / 2 : 100 + await this.page.mouse.click(1, y, { clickCount: 2, delay: 100 }) + await this.container.waitFor({ state: "hidden" }) + } +} diff --git a/e2e/tests/public/search.spec.ts b/e2e/tests/public/search.spec.ts new file mode 100644 index 000000000..c52b46d47 --- /dev/null +++ b/e2e/tests/public/search.spec.ts @@ -0,0 +1,71 @@ +import { test, expect } from "../../index" + +test.describe("Search tests", async () => { + test("Searching for a specific product returns the correct product page", async ({ + productPage, + }) => { + const searchModal = productPage.searchModal + await searchModal.open() + await searchModal.searchInput.fill("Sweatshirt") + await searchModal.searchResult + .filter({ hasText: "Sweatshirt" }) + .first() + .click() + await productPage.container.waitFor({ state: "visible" }) + await expect(productPage.productTitle).toContainText("Sweatshirt") + }) + + test("An erroneous search returns an empty result", async ({ + productPage, + }) => { + const searchModal = productPage.searchModal + await searchModal.open() + await searchModal.searchInput.fill("Does Not Sweatshirt") + await expect(searchModal.noSearchResultsContainer).toBeVisible() + }) + + test("User can search after an empty search result", async ({ + productPage, + }) => { + const searchModal = productPage.searchModal + + await searchModal.open() + await searchModal.searchInput.fill("Does Not Sweatshirt") + await expect(searchModal.noSearchResultsContainer).toBeVisible() + + await searchModal.searchInput.fill("Sweat") + await expect(searchModal.searchResults).toBeVisible() + await expect(searchModal.searchResult.first()).toBeVisible() + }) + + test("Closing the search page returns user back to their current page", async ({ + storePage, + productPage, + loginPage, + }) => { + const searchModal = storePage.searchModal + await test.step("Navigate to the store page and open and close search modal", async () => { + await storePage.goto() + await searchModal.open() + await searchModal.close() + await expect(storePage.container).toBeVisible() + }) + + await test.step("Navigate to the product page and open and close search modal", async () => { + await storePage.goto() + const product = await storePage.getProduct("Sweatshirt") + await product.locator.click() + await productPage.container.waitFor({ state: "visible" }) + await searchModal.open() + await searchModal.close() + await expect(productPage.container).toBeVisible() + }) + + await test.step("Navigate to the login page and open and close search modal", async () => { + await loginPage.goto() + await searchModal.open() + await searchModal.close() + await expect(loginPage.container).toBeVisible() + }) + }) +}) diff --git a/src/modules/search/components/hit/index.tsx b/src/modules/search/components/hit/index.tsx index fadf97cc9..a1338ca13 100644 --- a/src/modules/search/components/hit/index.tsx +++ b/src/modules/search/components/hit/index.tsx @@ -21,7 +21,10 @@ type HitProps = { const Hit = ({ hit }: HitProps) => { return ( - + { />
- {hit.title} + + {hit.title} +
diff --git a/src/modules/search/components/hits/index.tsx b/src/modules/search/components/hits/index.tsx index da184a8c7..bab97f120 100644 --- a/src/modules/search/components/hits/index.tsx +++ b/src/modules/search/components/hits/index.tsx @@ -33,7 +33,10 @@ const Hits = ({ } )} > -
+
{hits.slice(0, 6).map((hit, index) => (
  • { if (hits.length === 0) { return ( - + No results found. ) diff --git a/src/modules/search/templates/search-modal/index.tsx b/src/modules/search/templates/search-modal/index.tsx index b63cc3e61..068031a36 100644 --- a/src/modules/search/templates/search-modal/index.tsx +++ b/src/modules/search/templates/search-modal/index.tsx @@ -63,7 +63,10 @@ export default function SearchModal() { indexName={SEARCH_INDEX_NAME} searchClient={searchClient} > -
    +