Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 49 additions & 1 deletion assets/js/api-search/src/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,54 @@
*/
import { getArgsWithoutConstraints } from './utilities';

/**
* Reorder search results to respect Custom Results ordering.
*
* Posts with an ep_custom_result taxonomy term matching the search term
* are placed at their specified term_order positions. Mirrors the PHP
* SearchOrdering::posts_results() logic.
*
* @param {Array} hits Elasticsearch hits array.
* @param {string} searchTerm Current search term.
* @returns {Array} Reordered hits.
*/
const applyCustomResultsOrdering = (hits, searchTerm) => {
if (!searchTerm || !hits.length) {
return hits;
}

const normalizedTerm = searchTerm.toLowerCase();
const toInject = {};
const remaining = [];

for (const hit of hits) {
const customResults = hit._source?.terms?.ep_custom_result;
const matchingTerm = Array.isArray(customResults)
? customResults.find((term) => term.name && term.name.toLowerCase() === normalizedTerm)
: null;

if (matchingTerm && matchingTerm.term_order) {
toInject[matchingTerm.term_order] = hit;
} else {
remaining.push(hit);
}
}

if (!Object.keys(toInject).length) {
return hits;
}

const sortedPositions = Object.keys(toInject)
.map(Number)
.sort((a, b) => a - b);

for (const position of sortedPositions) {
remaining.splice(position - 1, 0, toInject[position]);
}

return remaining;
};

/**
* Reducer function.
*
Expand Down Expand Up @@ -74,7 +122,7 @@ export default (state, action) => {
const totalNumber = typeof total === 'number' ? total : total.value;

newState.aggregations = aggregations;
newState.searchResults = hits;
newState.searchResults = applyCustomResultsOrdering(hits, newState.args.search);
newState.searchTerm = newState.args.search;
newState.totalResults = totalNumber;
newState.suggestedTerms = suggest?.ep_suggestion?.[0]?.options || [];
Expand Down
83 changes: 83 additions & 0 deletions tests/e2e/src/specs/instant-results.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,89 @@ test.describe('Instant Results Feature', { tag: '@group1' }, () => {
);
});

test('Custom Results pointer order is respected in Instant Results', async ({
loggedInPage,
}) => {
const searchTerm = 'block';

await maybeEnableFeature('instant-results');
await maybeEnableFeature('searchordering');
await wpCli('elasticpress put-search-template', true);

// Remove any existing pointers to avoid pollution from prior tests.
await wpCliEval(`
$ep_pointers = get_posts(
[
'post_type' => 'ep-pointer',
'numberposts' => 999,
]
);
foreach ( $ep_pointers as $pointer ) {
wp_delete_post( $pointer->ID, true );
}
`);

/**
* Create a pointer that pins results for the search term in a
* non-default order.
*/
await goToAdminPage(loggedInPage, 'post-new.php?post_type=ep-pointer');

const previewResponsePromise = loggedInPage.waitForResponse(
'**/wp-json/elasticpress/v1/pointer_preview*',
);
await loggedInPage.locator('#titlewrap input').pressSequentially(searchTerm);
await previewResponsePromise;

const initialOrder = await loggedInPage
.locator('.pointers .pointer .title')
.allTextContents();
expect(initialOrder.length).toBeGreaterThan(1);

// Swap the first two results via keyboard drag.
await loggedInPage
.locator('.pointers > div')
.first()
.locator('.dashicons-menu')
.focus();
await loggedInPage.keyboard.press('Space');
await loggedInPage.keyboard.press('ArrowDown');
await loggedInPage.keyboard.press('Space');

const expectedOrder = await loggedInPage
.locator('.pointers .pointer .title')
.allTextContents();
expect(expectedOrder).not.toEqual(initialOrder);

await loggedInPage.click('#publish');
await loggedInPage.waitForLoadState('networkidle');

// Allow Elasticsearch time to index the pointer assignments.
await loggedInPage.waitForTimeout(2000);

/**
* Search via Instant Results and verify the modal returns the
* pinned posts in the order configured on the pointer.
*/
const responsePromise = instantResultRequestPromise(
loggedInPage,
`search=${searchTerm}`,
);
await loggedInPage.goto('/');
await searchFor(loggedInPage, searchTerm);
await expect(loggedInPage.locator('.ep-search-modal')).toBeVisible();
await responsePromise;
await expect(loggedInPage.locator('.ep-search-result').first()).toBeVisible();

const irTitles = await loggedInPage
.locator('.ep-search-result__title')
.allTextContents();

for (let index = 0; index < expectedOrder.length; index++) {
expect(irTitles[index]).toBe(expectedOrder[index]);
}
});

test('Is possible to set the default post type from a search form', async ({
loggedInPage,
}) => {
Expand Down
Loading