Skip to content

Commit

Permalink
fix(core): Ensure deterministic sorting in case of duplicates (#2632)
Browse files Browse the repository at this point in the history
  • Loading branch information
hans-rollingridges-dev authored Jan 22, 2024
1 parent 0c645e3 commit 81b4607
Show file tree
Hide file tree
Showing 9 changed files with 35,883 additions and 5,449 deletions.
19 changes: 1 addition & 18 deletions packages/core/e2e/default-search-plugin.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { initialData } from '../../../e2e-common/e2e-initial-data';
import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';

import { SEARCH_PRODUCTS_ADMIN } from './graphql/admin-definitions';
import {
ChannelFragment,
CurrencyCode,
Expand Down Expand Up @@ -1920,24 +1921,6 @@ export const REINDEX = gql`
}
`;

export const SEARCH_PRODUCTS_ADMIN = gql`
query SearchProductsAdmin($input: SearchInput!) {
search(input: $input) {
totalItems
items {
enabled
productId
productName
slug
description
productVariantId
productVariantName
sku
}
}
}
`;

export const SEARCH_GET_FACET_VALUES = gql`
query SearchFacetValues($input: SearchInput!) {
search(input: $input) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { DefaultJobQueuePlugin, DefaultSearchPlugin, mergeConfig } from '@vendure/core';
import { createTestEnvironment } from '@vendure/testing';
import path from 'path';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';

import { initialData } from '../../../../e2e-common/e2e-initial-data';
import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../../e2e-common/test-config';
import { SEARCH_PRODUCTS_ADMIN } from '../graphql/admin-definitions';
import {
SearchResultSortParameter,
SortOrder,
SearchProductsAdminQuery,
SearchProductsAdminQueryVariables,
} from '../graphql/generated-e2e-admin-types';
import {
SearchProductsShopQuery,
SearchProductsShopQueryVariables,
} from '../graphql/generated-e2e-shop-types';
import { SEARCH_PRODUCTS_SHOP } from '../graphql/shop-definitions';
import { awaitRunningJobs } from '../utils/await-running-jobs';

describe('Default search plugin', () => {
const { server, adminClient, shopClient } = createTestEnvironment(
mergeConfig(testConfig(), {
plugins: [
DefaultSearchPlugin.init({
indexStockStatus: true,
}),
DefaultJobQueuePlugin,
],
}),
);

beforeAll(async () => {
await server.init({
initialData,
productsCsvPath: path.join(__dirname, 'fixtures', 'default-search-plugin-sort-by.csv'),
customerCount: 1,
});
await adminClient.asSuperAdmin();
await awaitRunningJobs(adminClient);
}, TEST_SETUP_TIMEOUT_MS);

afterAll(async () => {
await awaitRunningJobs(adminClient);
await server.destroy();
});

function searchProductsShop(queryVariables: SearchProductsShopQueryVariables) {
return shopClient.query<SearchProductsShopQuery, SearchProductsShopQueryVariables>(
SEARCH_PRODUCTS_SHOP,
queryVariables,
);
}

function searchProductsAdmin(queryVariables: SearchProductsAdminQueryVariables) {
return adminClient.query<SearchProductsAdminQuery, SearchProductsAdminQueryVariables>(
SEARCH_PRODUCTS_ADMIN,
queryVariables,
);
}

type SearchProducts = (
queryVariables: SearchProductsShopQueryVariables | SearchProductsAdminQueryVariables,
) => Promise<SearchProductsShopQuery | SearchProductsAdminQuery>;

async function testSearchProducts(
searchProducts: SearchProducts,
groupByProduct: boolean,
sortBy: keyof SearchResultSortParameter,
sortOrder: (typeof SortOrder)[keyof typeof SortOrder],
skip: number,
take: number,
) {
return searchProducts({
input: {
groupByProduct,
sort: {
[sortBy]: sortOrder,
},
skip,
take,
},
});
}

async function testSortByPriceAsc(searchProducts: SearchProducts) {
const resultPage1 = await testSearchProducts(searchProducts, false, 'price', SortOrder.ASC, 0, 3);
const resultPage2 = await testSearchProducts(searchProducts, false, 'price', SortOrder.ASC, 3, 3);

const pvIds1 = resultPage1.search.items.map(i => i.productVariantId);
const pvIds2 = resultPage2.search.items.map(i => i.productVariantId);
const pvIds3 = pvIds1.concat(pvIds2);

expect(new Set(pvIds3).size).equals(6);
expect(resultPage1.search.items.map(i => i.productVariantId)).toEqual(['T_4', 'T_5', 'T_6']);
expect(resultPage2.search.items.map(i => i.productVariantId)).toEqual(['T_7', 'T_8', 'T_9']);
}

async function testSortByPriceDesc(searchProducts: SearchProducts) {
const resultPage1 = await testSearchProducts(searchProducts, false, 'price', SortOrder.DESC, 0, 3);
const resultPage2 = await testSearchProducts(searchProducts, false, 'price', SortOrder.DESC, 3, 3);

const pvIds1 = resultPage1.search.items.map(i => i.productVariantId);
const pvIds2 = resultPage2.search.items.map(i => i.productVariantId);
const pvIds3 = pvIds1.concat(pvIds2);

expect(new Set(pvIds3).size).equals(6);
expect(resultPage1.search.items.map(i => i.productVariantId)).toEqual(['T_1', 'T_2', 'T_3']);
expect(resultPage2.search.items.map(i => i.productVariantId)).toEqual(['T_4', 'T_5', 'T_6']);
}

async function testSortByPriceAscGroupByProduct(searchProducts: SearchProducts) {
const resultPage1 = await testSearchProducts(searchProducts, true, 'price', SortOrder.ASC, 0, 3);
const resultPage2 = await testSearchProducts(searchProducts, true, 'price', SortOrder.ASC, 3, 3);

const pvIds1 = resultPage1.search.items.map(i => i.productVariantId);
const pvIds2 = resultPage2.search.items.map(i => i.productVariantId);
const pvIds3 = pvIds1.concat(pvIds2);

expect(new Set(pvIds3).size).equals(6);
expect(resultPage1.search.items.map(i => i.productId)).toEqual(['T_4', 'T_5', 'T_6']);
expect(resultPage2.search.items.map(i => i.productId)).toEqual(['T_1', 'T_2', 'T_3']);
}

async function testSortByPriceDescGroupByProduct(searchProducts: SearchProducts) {
const resultPage1 = await testSearchProducts(searchProducts, true, 'price', SortOrder.DESC, 0, 3);
const resultPage2 = await testSearchProducts(searchProducts, true, 'price', SortOrder.DESC, 3, 3);

const pvIds1 = resultPage1.search.items.map(i => i.productVariantId);
const pvIds2 = resultPage2.search.items.map(i => i.productVariantId);
const pvIds3 = pvIds1.concat(pvIds2);

expect(new Set(pvIds3).size).equals(6);
expect(resultPage1.search.items.map(i => i.productId)).toEqual(['T_1', 'T_2', 'T_3']);
expect(resultPage2.search.items.map(i => i.productId)).toEqual(['T_4', 'T_5', 'T_6']);
}

describe('Search products shop', () => {
const searchProducts = searchProductsShop;

it('sort by price ASC', () => testSortByPriceAsc(searchProducts));
it('sort by price DESC', () => testSortByPriceDesc(searchProducts));

it('sort by price ASC group by product', () => testSortByPriceAscGroupByProduct(searchProducts));
it('sort by price DESC group by product', () => testSortByPriceDescGroupByProduct(searchProducts));
});

describe('Search products admin', () => {
const searchProducts = searchProductsAdmin;

it('sort by price ACS', () => testSortByPriceAsc(searchProducts));
it('sort by price DESC', () => testSortByPriceDesc(searchProducts));

it('sort by price ASC group by product', () => testSortByPriceAscGroupByProduct(searchProducts));
it('sort by price DESC group by product', () => testSortByPriceDescGroupByProduct(searchProducts));
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name,slug,description,assets,facets,optionGroups,optionValues,sku,price,taxCategory,stockOnHand,trackInventory,variantAssets,variantFacets
Boot A,boot-a,Boot A Size 40,,category:sports equipment,shoe size,Size 40,BA40,100,standard,100,true,,
Boot B,boot-b,Boot B Size 40,,category:sports equipment,shoe size,Size 40,BB40,100,standard,100,true,,
Boot C,boot-c,Boot C Size 40,,category:sports equipment,shoe size,Size 40,BC40,100,standard,100,true,,
Sneaker A,sneaker-a,Sneaker A Size 40,,category:sports equipment,shoe size,Size 40,SA40,99.99,standard,100,true,,
,,Sneaker A Size 41,,,,Size 41,SA41,99.99,standard,100,true,,
,,Sneaker A Size 42,,,,Size 42,SA42,99.99,standard,100,true,,
,,Sneaker A Size 43,,,,Size 43,SA43,99.99,standard,100,true,,
Sneaker B,sneaker-b,Sneaker B Size 40,,category:sports equipment,shoe size,Size 40,SB40,99.99,standard,100,true,,
,,Sneaker B Size 41,,,,Size 41,SB41,99.99,standard,100,true,,
,,Sneaker B Size 42,,,,Size 42,SB42,99.99,standard,100,true,,
,,Sneaker B Size 43,,,,Size 43,SB43,99.99,standard,100,true,,
Sneaker C,sneaker-c,Sneaker C Size 40,,category:sports equipment,shoe size,Size 40,SC40,99.99,standard,100,true,,
,,Sneaker C Size 41,,,,Size 41,SC41,99.99,standard,100,true,,
,,Sneaker C Size 42,,,,Size 42,SC42,99.99,standard,100,true,,
,,Sneaker C Size 43,,,,Size 43,SC43,99.99,standard,100,true,,
19 changes: 19 additions & 0 deletions packages/core/e2e/graphql/admin-definitions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import gql from 'graphql-tag';

export const SEARCH_PRODUCTS_ADMIN = gql`
query SearchProductsAdmin($input: SearchInput!) {
search(input: $input) {
totalItems
items {
enabled
productId
productName
slug
description
productVariantId
productVariantName
sku
}
}
}
`;
Loading

0 comments on commit 81b4607

Please sign in to comment.