Repository: shopware/frontends (ts)

Issue:
[BUG] useListing missing error handling
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Current Behavior

Currently calling useListing-composable for example from a component, unhandled error happens.

**See code below:**

```ts
/**
 * @public
 */
export function useListing(params?: {
  listingType: ListingType;
  categoryId?: string;
  defaultSearchCriteria?: RequestParameters<"searchPage">;
}): UseListingReturn {
  const listingType = params?.listingType || "categoryListing";

  // const { getDefaults } = useDefaults({ defaultsKey: contextName }); // ⚠️ necessary?
  const { apiClient } = useShopwareContext();

  let searchMethod;
  if (listingType === "productSearchListing") {
    searchMethod = async (searchCriteria: RequestParameters<"searchPage">) => {
      return apiClient.invoke("searchPage post /search", {
        ...searchCriteria,
      });
    };
  } else {
    const { category } = useCategory(); // 🚨 This causes unhandled error when category context is not present.

    searchMethod = async (searchCriteria: RequestParameters<"searchPage">) => {
      if (!category.value?.id) {
        throw new Error(
          "[useListing][search] Search category id does not exist.",
        );
      }
      return apiClient.invoke(
        "readProductListing post /product-listing/{categoryId} sw-include-seo-urls",
        {
          "sw-include-seo-urls": true,
          ...searchCriteria,
          categoryId: category.value.id,
        },
      );
    };
  }

  return createListingComposable({
    listingKey: listingType,
    searchMethod,
    searchDefaults:
      params?.defaultSearchCriteria || ({} as RequestParameters<"searchPage">), //getDefaults(),
  });
}
```

### Expected Behavior

If Category context is not present, the code should use the params.categoryId if exists. If params.categoryId does not exsist, then it should cracefully throw an handled error.

** Fix proposition **
```ts
/**
 * @public
 */
export function useListing(params?: {
  listingType: ListingType;
  categoryId?: string;
  defaultSearchCriteria?: RequestParameters<"searchPage">;
}): UseListingReturn {
  const listingType = params?.listingType || "categoryListing";

  const { apiClient } = useShopwareContext();

  let searchMethod;
  if (listingType === "productSearchListing") {
    searchMethod = async (searchCriteria: RequestParameters<"searchPage">) => {
      return apiClient.invoke("searchPage post /search", {
        ...searchCriteria,
      });
    };
  } else {
    let resourceId: string | undefined;
    try {
      const { category } = useCategory();
      resourceId = category.value?.id;
    }catch(error) {
      if (error instanceof ContextError) {
        resourceId = params?.categoryId;
      } else {
        console.error(error);
      }
    }

    searchMethod = async (searchCriteria: RequestParameters<"searchPage">) => {
      if (!resourceId) {
        throw new Error(
          "[useListing][search] Search category id does not exist.",
        );
      }
      return apiClient.invoke(
        "readProductListing post /product-listing/{categoryId} sw-include-seo-urls",
        {
          "sw-include-seo-urls": true,
          ...searchCriteria,
          categoryId: resourceId,
        },
      );
    };
  }

  return createListingComposable({
    listingKey: listingType,
    searchMethod,
    searchDefaults:
      params?.defaultSearchCriteria || ({} as RequestParameters<"searchPage">), //getDefaults(),
  });
}
```

### Steps To Reproduce

1. Create Nuxt project with Shopware frontends modules and add proper configs
2. Create some page
3. Create component `listing-example.vue`
4. Add following code to the component

```vue
<template>
<!-- No need to add anything here in this example -->
</template>
<script lang="ts" setup>

const {
    search,
    getElements,
} = useListing({
    listingType: "categoryListing",
    categoryId: <your-category-id>, // entrypoint to browse
    defaultSearchCriteria: { // set the default criteria
        search: "", // Add the 'search' property with an empty string value
        limit: 25,
        p: 1,
    },
});

await search({ search: '' });
</script>

### Environment

```markdown
- OS: Latest MacOS Sonoma
- Node: 18.18.0
- Yarn: 1.22.19
```


### Anything else?

_No response_

Pull request description:
fix(composables): use param category id
### Description

Handle using categoryId as an alternative for category context


closes #908

### Type of change

<!-- Comment out the type of change your PR is making -->

<!-- Bug fix (non-breaking change that fixes an issue) -->
<!-- New feature (non-breaking change which adds functionality) -->
<!-- Breaking change (fix or feature that would cause existing functionality to not work as expected) -->

### ToDo's

<!-- Add the todo's that need to be done before merge -->
<!-- Remember to run `pnpm run changeset` and describe your change (plus potential migration guide/important notes) to your pull request. -->

<!-- - [ ] Changeset file provided [read more](https://github.com/shopware/frontends/blob/main/CONTRIBUTION.md#changelog-preparation) -->
<!-- - [ ] Documentation added/updated -->
<!-- - [ ] Unit-Tests added/updated -->
<!-- - [ ] E2E-Tests added/updated -->
<!-- - [ ] Related Issue updated -->

### Screenshots (if applicable)

<!-- Please attach any relevant screenshots or images to help explain your changes. -->

### Additional context

<!-- Add any other context about the pull request here. -->


Patch:
diff --git a/.changeset/cyan-guests-try.md b/.changeset/cyan-guests-try.md
new file mode 100644
index 00000000..07a9e7c9
--- /dev/null
+++ b/.changeset/cyan-guests-try.md
@@ -0,0 +1,5 @@
+---
+"@shopware-pwa/composables-next": patch
+---
+
+Handle using categoryId as a alternative for category context
diff --git a/packages/composables/src/mocks/Search.ts b/packages/composables/src/mocks/Search.ts
new file mode 100644
index 00000000..48e1b9f5
--- /dev/null
+++ b/packages/composables/src/mocks/Search.ts
@@ -0,0 +1,518 @@
+const Search = {
+  extensions: {
+    multiSearchResult: {
+      searchResults: {
+        product_manufacturer: {
+          elements: [],
+          aggregations: [],
+          page: 1,
+          limit: 30,
+          entity: "product_manufacturer",
+          total: 0,
+          states: [],
+          apiAlias: "dal_entity_search_result",
+        },
+        category: {
+          elements: [],
+          aggregations: [],
+          page: 1,
+          limit: 30,
+          entity: "category",
+          total: 0,
+          states: [],
+          apiAlias: "dal_entity_search_result",
+        },
+      },
+      apiAlias:
+        "shopware_commercial_advanced_search_domain_search_multi_search_result",
+    },
+  },
+  elements: [
+    {
+      extensions: {
+        foreignKeys: {
+          swagCustomizedProductsTemplateId: null,
+          swagCustomizedProductsTemplateVersionId:
+            "0fa91ce3e96a4bc2be4bd9ce752c3425",
+          apiAlias: "product_foreign_keys_extension",
+        },
+        search: {
+          extensions: [],
+          _uniqueIdentifier: null,
+          translated: [],
+          id: "81080519182c496f86722fa417f9106b",
+          productNumber: "SW10069",
+          autoIncrement: 28,
+          _score: "280",
+        },
+      },
+      versionId: "0fa91ce3e96a4bc2be4bd9ce752c3425",
+      translated: {
+        metaDescription: null,
+        name: "\u00bbArctic Rose\u00ab IKARIMI\u00ae salmon filet",
+        keywords: null,
+        description:
+          "\u003Cp\u003ELorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu.\u003C/p\u003E\n\u003Cp\u003EIn enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibus. Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus.\u003C/p\u003E\n\u003Cp\u003EPhasellus viverra nulla ut metus varius laoreet. Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi. Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero, sit amet adipiscing sem neque sed ipsum. Nam quam nunc, blandit vel, luctus pulvinar, hendrerit id, lorem.\u003C/p\u003E\n\u003Cp\u003EMaecenas nec odio et ante tincidunt tempus. Donec vitae sapien ut libero venenatis faucibus. Nullam quis ante. Etiam sit amet orci eget eros faucibus tincidunt. Duis leo. Sed fringilla mauris sit amet nibh. Donec sodales sagittis magna. Sed consequat, leo eget bibendum sodales, augue velit cursus nunc, quis gravida magna mi a libero. Fusce vulputate eleifend sapien. Vestibulum purus quam, scelerisque ut, mollis sed, nonummy id, metus. Nullam accumsan lorem in dui. Cras ultricies mi eu turpis hendrerit fringilla. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; In ac dui quis mi.\u003C/p\u003E",
+        metaTitle: null,
+        packUnit: null,
+        packUnitPlural: null,
+        customFields: {},
+      },
+      createdAt: "2020-08-06T06:26:32.805+00:00",
+      updatedAt: "2024-03-12T10:26:08.297+00:00",
+      parentId: null,
+      childCount: 0,
+      taxId: "d55571c0b59a4a538fe44f16a3a206c3",
+      manufacturerId: "432878e43ca440aab3afb211b2b79aba",
+      unitId: "83baf03daaeb44a6b736e99c85d4c102",
+      active: true,
+      displayGroup: "c3405c060fb26fe8a4a53329bedc46e4",
+      manufacturerNumber: null,
+      ean: null,
+      sales: 11,
+      productNumber: "SW10069",
+      stock: -56,
+      availableStock: -56,
+      available: true,
+      deliveryTimeId: "0caaf7f6131142f6b1fa81f3e3f6c0fb",
+      deliveryTime: {
+        translated: {
+          name: "1-3 days",
+          customFields: {},
+        },
+        createdAt: "2020-08-06T06:26:54.178+00:00",
+        updatedAt: "2020-08-06T06:26:54.182+00:00",
+        name: "1-3 days",
+        min: 1,
+        max: 3,
+        unit: "day",
+        customFields: null,
+        id: "0caaf7f6131142f6b1fa81f3e3f6c0fb",
+        apiAlias: "delivery_time",
+      },
+      restockTime: 1,
+      isCloseout: false,
+      purchaseSteps: 1,
+      maxPurchase: null,
+      minPurchase: 1,
+      purchaseUnit: 0.8,
+      referenceUnit: 1,
+      shippingFree: true,
+      markAsTopseller: false,
+      weight: null,
+      width: null,
+      height: null,
+      length: null,
+      releaseDate: null,
+      categoryTree: [
+        "525abe8981214bd2ba94fd33942333ec",
+        "bda4b60e845240b2b9d6b60e71196e14",
+        "ca15f1e906204da49afe666bbcca7825",
+        "fdc5f48ae7064410a07587fd7b8c3deb",
+        "eebe32ca93e94b7590c65a55a61d4cf9",
+      ],
+      streamIds: [
+        "1318833f46df457981763b94179d9ef0",
+        "5675448d6b3a4dd2a659d31a3dde74da",
+      ],
+      optionIds: null,
+      propertyIds: null,
+      name: "\u00bbArctic Rose\u00ab IKARIMI\u00ae salmon filet",
+      keywords: null,
+      description:
+        "\u003Cp\u003ELorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu.\u003C/p\u003E\n\u003Cp\u003EIn enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibus. Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus.\u003C/p\u003E\n\u003Cp\u003EPhasellus viverra nulla ut metus varius laoreet. Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi. Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero, sit amet adipiscing sem neque sed ipsum. Nam quam nunc, blandit vel, luctus pulvinar, hendrerit id, lorem.\u003C/p\u003E\n\u003Cp\u003EMaecenas nec odio et ante tincidunt tempus. Donec vitae sapien ut libero venenatis faucibus. Nullam quis ante. Etiam sit amet orci eget eros faucibus tincidunt. Duis leo. Sed fringilla mauris sit amet nibh. Donec sodales sagittis magna. Sed consequat, leo eget bibendum sodales, augue velit cursus nunc, quis gravida magna mi a libero. Fusce vulputate eleifend sapien. Vestibulum purus quam, scelerisque ut, mollis sed, nonummy id, metus. Nullam accumsan lorem in dui. Cras ultricies mi eu turpis hendrerit fringilla. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; In ac dui quis mi.\u003C/p\u003E",
+      metaDescription: null,
+      metaTitle: null,
+      packUnit: null,
+      packUnitPlural: null,
+      tax: {
+        translated: [],
+        createdAt: "2020-08-06T06:23:32.056+00:00",
+        updatedAt: "2020-08-06T06:26:46.462+00:00",
+        taxRate: 19,
+        name: "19%",
+        position: 0,
+        customFields: null,
+        id: "d55571c0b59a4a538fe44f16a3a206c3",
+        apiAlias: "tax",
+      },
+      manufacturer: {
+        versionId: "0fa91ce3e96a4bc2be4bd9ce752c3425",
+        translated: {
+          name: "Deutsche See",
+          description:
+            "\u003Cp\u003E  You can find further information about this manufacturer \u003Ca href=\u0022http://www.deutschesee.de/\u0022 target=\u0022_blank\u0022\u003Ehere\u003C/a\u003E.\u0026nbsp;\u003C/p\u003E",
+          customFields: {},
+        },
+        createdAt: "2020-08-06T06:26:30.597+00:00",
+        updatedAt: null,
+        mediaId: "7fe01085296d4ece9d3738d781d287ca",
+        name: "Deutsche See",
+        link: "http://www.deutschesee.de/",
+        description:
+          "\u003Cp\u003E  You can find further information about this manufacturer \u003Ca href=\u0022http://www.deutschesee.de/\u0022 target=\u0022_blank\u0022\u003Ehere\u003C/a\u003E.\u0026nbsp;\u003C/p\u003E",
+        media: null,
+        translations: null,
+        customFields: null,
+        id: "432878e43ca440aab3afb211b2b79aba",
+        apiAlias: "product_manufacturer",
+      },
+      unit: {
+        translated: {
+          shortCode: "kg",
+          name: "kilogramme",
+          customFields: {},
+        },
+        createdAt: "2020-08-06T06:26:30.701+00:00",
+        updatedAt: null,
+        shortCode: "kg",
+        name: "kilogramme",
+        customFields: null,
+        id: "83baf03daaeb44a6b736e99c85d4c102",
+        apiAlias: "unit",
+      },
+      cover: {
+        versionId: "0fa91ce3e96a4bc2be4bd9ce752c3425",
+        translated: [],
+        createdAt: "2020-08-06T06:26:32.797+00:00",
+        updatedAt: "2023-06-26T18:52:41.032+00:00",
+        productId: "81080519182c496f86722fa417f9106b",
+        mediaId: "934204c929db48fdb9feba2a75563c65",
+        position: 0,
+        media: {
+          translated: {
+            alt: null,
+            title: null,
+            customFields: {},
+          },
+          createdAt: "2020-08-06T06:25:58.986+00:00",
+          updatedAt: "2020-08-06T06:26:32.797+00:00",
+          mimeType: "image/jpeg",
+          fileExtension: "jpg",
+          fileSize: 170827,
+          title: null,
+          metaData: {
+            type: 2,
+            width: 1010,
+            height: 730,
+          },
+          uploadedAt: null,
+          alt: null,
+          url: "https://cdn.shopware.store/a/B/m/pPkDE/media/b1/48/d9/SW10069.jpg?1596695192",
+          fileName: "SW10069",
+          translations: null,
+          thumbnails: [
+            {
+              translated: [],
+              createdAt: "2020-08-06T06:25:58.984+00:00",
+              updatedAt: "2020-08-06T06:26:32.795+00:00",
+              width: 400,
+              path: "thumbnail/b1/48/d9/SW10069_400x400.jpg",
+              height: 400,
+              url: "https://cdn.shopware.store/a/B/m/pPkDE/thumbnail/b1/48/d9/SW10069_400x400.jpg?1596695192",
+              mediaId: "934204c929db48fdb9feba2a75563c65",
+              customFields: null,
+              id: "83853fda8fbe4d38a2f8c32d1bac2e5a",
+              apiAlias: "media_thumbnail",
+            },
+            {
+              translated: [],
+              createdAt: "2020-08-06T06:25:58.985+00:00",
+              updatedAt: "2020-08-06T06:26:32.796+00:00",
+              width: 1920,
+              path: "thumbnail/b1/48/d9/SW10069_1920x1920.jpg",
+              height: 1920,
+              url: "https://cdn.shopware.store/a/B/m/pPkDE/thumbnail/b1/48/d9/SW10069_1920x1920.jpg?1596695192",
+              mediaId: "934204c929db48fdb9feba2a75563c65",
+              customFields: null,
+              id: "a7101873fcfd4380b964ca6dc4fdd771",
+              apiAlias: "media_thumbnail",
+            },
+            {
+              translated: [],
+              createdAt: "2020-08-06T06:25:58.986+00:00",
+              updatedAt: "2020-08-06T06:26:32.797+00:00",
+              width: 800,
+              path: "thumbnail/b1/48/d9/SW10069_800x800.jpg",
+              height: 800,
+              url: "https://cdn.shopware.store/a/B/m/pPkDE/thumbnail/b1/48/d9/SW10069_800x800.jpg?1596695192",
+              mediaId: "934204c929db48fdb9feba2a75563c65",
+              customFields: null,
+              id: "de7a1b3d975e495fb94b0edee64e3ac9",
+              apiAlias: "media_thumbnail",
+            },
+          ],
+          hasFile: true,
+          private: false,
+          path: "media/b1/48/d9/SW10069.jpg",
+          config: null,
+          customFields: null,
+          id: "934204c929db48fdb9feba2a75563c65",
+          apiAlias: "media",
+        },
+        customFields: null,
+        id: "0840c89c4d834c529f28d273ea8990cd",
+        apiAlias: "product_media",
+      },
+      parent: null,
+      children: null,
+      media: null,
+      cmsPageId: "7a6d253a67204037966f42b0119704d5",
+      cmsPage: null,
+      translations: null,
+      categories: null,
+      tags: null,
+      properties: null,
+      options: [],
+      configuratorSettings: null,
+      categoriesRo: null,
+      coverId: "0840c89c4d834c529f28d273ea8990cd",
+      tagIds: null,
+      categoryIds: ["eebe32ca93e94b7590c65a55a61d4cf9"],
+      productReviews: null,
+      ratingAverage: null,
+      mainCategories: null,
+      seoUrls: null,
+      crossSellings: null,
+      canonicalProductId: null,
+      canonicalProduct: null,
+      streams: null,
+      downloads: null,
+      states: ["is-physical"],
+      customFields: {},
+      id: "81080519182c496f86722fa417f9106b",
+      calculatedPrices: [],
+      calculatedPrice: {
+        unitPrice: 39.9,
+        quantity: 1,
+        totalPrice: 39.9,
+        calculatedTaxes: [
+          {
+            tax: 2.61,
+            taxRate: 7,
+            price: 39.9,
+            apiAlias: "cart_tax_calculated",
+          },
+        ],
+        taxRules: [
+          {
+            taxRate: 7,
+            percentage: 100,
+            apiAlias: "cart_tax_rule",
+          },
+        ],
+        referencePrice: {
+          purchaseUnit: 0.8,
+          referenceUnit: 1,
+          unitName: "kilogramme",
+          price: 49.88,
+          apiAlias: "cart_price_reference",
+        },
+        listPrice: null,
+        regulationPrice: {
+          price: 10,
+          apiAlias: "cart_regulation_price",
+        },
+        apiAlias: "calculated_price",
+      },
+      sortedProperties: null,
+      calculatedCheapestPrice: {
+        unitPrice: 39.9,
+        quantity: 1,
+        totalPrice: 39.9,
+        calculatedTaxes: [
+          {
+            tax: 2.61,
+            taxRate: 7,
+            price: 39.9,
+            apiAlias: "cart_tax_calculated",
+          },
+        ],
+        taxRules: [
+          {
+            taxRate: 7,
+            percentage: 100,
+            apiAlias: "cart_tax_rule",
+          },
+        ],
+        referencePrice: {
+          purchaseUnit: 0.8,
+          referenceUnit: 1,
+          unitName: "kilogramme",
+          price: 49.88,
+          apiAlias: "cart_price_reference",
+        },
+        listPrice: null,
+        regulationPrice: {
+          price: 10,
+          apiAlias: "cart_regulation_price",
+        },
+        hasRange: false,
+        variantId: "81080519182c496f86722fa417f9106b",
+        apiAlias: "calculated_cheapest_price",
+      },
+      isNew: false,
+      calculatedMaxPurchase: 100,
+      seoCategory: null,
+      apiAlias: "product",
+    },
+  ],
+  aggregations: {
+    manufacturer: {
+      name: "manufacturer",
+      entities: [
+        {
+          versionId: "0fa91ce3e96a4bc2be4bd9ce752c3425",
+          translated: {
+            name: "Deutsche See",
+            description:
+              "\u003Cp\u003E  You can find further information about this manufacturer \u003Ca href=\u0022http://www.deutschesee.de/\u0022 target=\u0022_blank\u0022\u003Ehere\u003C/a\u003E.\u0026nbsp;\u003C/p\u003E",
+            customFields: {},
+          },
+          createdAt: "2020-08-06T06:26:30.597+00:00",
+          updatedAt: null,
+          mediaId: "7fe01085296d4ece9d3738d781d287ca",
+          name: "Deutsche See",
+          link: "http://www.deutschesee.de/",
+          description:
+            "\u003Cp\u003E  You can find further information about this manufacturer \u003Ca href=\u0022http://www.deutschesee.de/\u0022 target=\u0022_blank\u0022\u003Ehere\u003C/a\u003E.\u0026nbsp;\u003C/p\u003E",
+          media: null,
+          translations: null,
+          customFields: null,
+          id: "432878e43ca440aab3afb211b2b79aba",
+          apiAlias: "product_manufacturer",
+        },
+      ],
+      apiAlias: "manufacturer_aggregation",
+    },
+    price: {
+      name: "price",
+      min: "39.9000",
+      max: "39.9000",
+      avg: null,
+      sum: null,
+      apiAlias: "price_aggregation",
+    },
+    rating: {
+      name: "rating",
+      max: null,
+      apiAlias: "rating_aggregation",
+    },
+    "shipping-free": {
+      name: "shipping-free",
+      max: "1",
+      apiAlias: "shipping-free_aggregation",
+    },
+    properties: {
+      name: "properties",
+      buckets: [
+        {
+          key: "",
+          count: 1,
+          apiAlias: "aggregation_bucket",
+        },
+      ],
+      apiAlias: "properties_aggregation",
+    },
+    options: {
+      name: "options",
+      buckets: [
+        {
+          key: "",
+          count: 1,
+          apiAlias: "aggregation_bucket",
+        },
+      ],
+      apiAlias: "options_aggregation",
+    },
+  },
+  page: 1,
+  limit: 15,
+  entity: "product",
+  total: 1,
+  states: [],
+  sorting: "score",
+  currentFilters: {
+    manufacturer: [],
+    price: {
+      min: 0,
+      max: 0,
+    },
+    rating: null,
+    "shipping-free": false,
+    properties: [],
+    search: "artic",
+  },
+  availableSortings: [
+    {
+      translated: {
+        label: "Name A-Z",
+      },
+      createdAt: "2020-09-25T07:31:29.882+00:00",
+      updatedAt: null,
+      key: "name-asc",
+      priority: 4,
+      label: "Name A-Z",
+      apiAlias: "product_sorting",
+    },
+    {
+      translated: {
+        label: "Name Z-A",
+      },
+      createdAt: "2020-09-25T07:31:29.882+00:00",
+      updatedAt: null,
+      key: "name-desc",
+      priority: 3,
+      label: "Name Z-A",
+      apiAlias: "product_sorting",
+    },
+    {
+      translated: {
+        label: "Price ascending",
+      },
+      createdAt: "2020-09-25T07:31:29.882+00:00",
+      updatedAt: null,
+      key: "price-asc",
+      priority: 2,
+      label: "Price ascending",
+      apiAlias: "product_sorting",
+    },
+    {
+      translated: {
+        label: "Price descending",
+      },
+      createdAt: "2020-09-25T07:31:29.882+00:00",
+      updatedAt: null,
+      key: "price-desc",
+      priority: 1,
+      label: "Price descending",
+      apiAlias: "product_sorting",
+    },
+    {
+      translated: {
+        label: "Topseller",
+      },
+      createdAt: "2020-10-22T12:21:04.045+00:00",
+      updatedAt: null,
+      key: "topseller",
+      priority: 0,
+      label: "Topseller",
+      apiAlias: "product_sorting",
+    },
+    {
+      translated: {
+        label: "Top results",
+      },
+      createdAt: "2020-09-25T07:31:29.882+00:00",
+      updatedAt: null,
+      key: "score",
+      priority: 0,
+      label: "Top results",
+      apiAlias: "product_sorting",
+    },
+  ],
+  streamId: null,
+  apiAlias: "product_listing",
+};
+
+export default Search;
diff --git a/packages/composables/src/useListing.ts b/packages/composables/src/useListing.ts
index d642380f..47b2cfc8 100644
--- a/packages/composables/src/useListing.ts
+++ b/packages/composables/src/useListing.ts
@@ -176,6 +176,7 @@ export function useListing(params?: {
   defaultSearchCriteria?: operations["searchPage post /search"]["body"];
 }): UseListingReturn {
   const listingType = params?.listingType || "categoryListing";
+  let categoryId = params?.categoryId || null;
 
   // const { getDefaults } = useDefaults({ defaultsKey: contextName });
   const { apiClient } = useShopwareContext();
@@ -198,12 +199,15 @@ export function useListing(params?: {
       return data;
     };
   } else {
-    const { category } = useCategory();
+    if (!categoryId) {
+      const { category } = useCategory();
+      categoryId = category.value?.id;
+    }
 
     searchMethod = async (
       searchCriteria: operations["readProductListing post /product-listing/{categoryId}"]["body"],
     ) => {
-      if (!category.value?.id) {
+      if (!categoryId) {
         throw new Error(
           "[useListing][search] Search category id does not exist.",
         );
@@ -215,7 +219,7 @@ export function useListing(params?: {
             "sw-include-seo-urls": true,
           },
           pathParams: {
-            categoryId: category.value.id,
+            categoryId,
           },
           body: searchCriteria,
         },


Test patch:
diff --git a/packages/composables/src/useListing.test.ts b/packages/composables/src/useListing.test.ts
new file mode 100644
index 00000000..f657f363
--- /dev/null
+++ b/packages/composables/src/useListing.test.ts
@@ -0,0 +1,71 @@
+import { describe, expect, it } from "vitest";
+import { useListing } from "./useListing";
+import { useSetup } from "./_test";
+import searchMock from "./mocks/Search";
+import ContextError from "./helpers/ContextError";
+
+describe("useListing", () => {
+  it("invoke search", async () => {
+    const { vm, injections } = await useSetup(() =>
+      useListing({
+        listingType: "productSearchListing",
+      }),
+    );
+    injections.apiClient.invoke.mockResolvedValue({ data: {} });
+    await vm.search({
+      search: "",
+    });
+
+    injections.apiClient.invoke.mockResolvedValue(searchMock);
+
+    expect(injections.apiClient.invoke).toHaveBeenCalledWith(
+      expect.stringContaining("search"),
+      expect.objectContaining({}),
+    );
+
+    expect(vm.getCurrentPage).toBe(1);
+    expect(vm.getTotalPagesCount).toBe(0);
+    expect(vm.getLimit).toBe(10);
+  });
+
+  it('invoke "readQuotes"', async () => {
+    const { vm, injections } = await useSetup(() =>
+      useListing({
+        listingType: "categoryListing",
+        categoryId: "1234",
+      }),
+    );
+    injections.apiClient.invoke.mockResolvedValue({ data: {} });
+    await vm.search({
+      search: "",
+    });
+    expect(injections.apiClient.invoke).toHaveBeenCalledWith(
+      expect.stringContaining("readProductListing"),
+      expect.objectContaining({
+        body: {
+          search: "",
+        },
+        headers: {
+          "sw-include-seo-urls": true,
+        },
+        pathParams: {
+          categoryId: "1234",
+        },
+      }),
+    );
+  });
+
+  it('invoke "readQuotes" - errors', async () => {
+    let error: unknown | null = null;
+    try {
+      await useSetup(() =>
+        useListing({
+          listingType: "categoryListing",
+        }),
+      );
+    } catch (e) {
+      error = e;
+    }
+    expect(error).toEqual(new ContextError("Category"));
+  });
+});