Skip to content

Commit

Permalink
chore(indexes): add search indexes sorting test; remove only; make su…
Browse files Browse the repository at this point in the history
…re tab state hooks is better adapted for testing env
  • Loading branch information
gribnoysup committed Mar 26, 2024
1 parent d226814 commit 560d55f
Show file tree
Hide file tree
Showing 4 changed files with 183 additions and 62 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { expect } from 'chai';

import { RegularIndexesTable } from './regular-indexes-table';
import type { RegularIndex } from '../../modules/regular-indexes';
import { mockRegularIndex } from '../../../test/helpers';

const indexes = [
{
Expand Down Expand Up @@ -93,21 +94,6 @@ const indexes = [
},
] as RegularIndex[];

function mockIndex(info: Partial<RegularIndex>): RegularIndex {
return {
ns: 'test.test',
name: '_id_1',
key: {},
fields: [],
size: 0,
relativeSize: 0,
...info,
extra: {
...info.extra,
},
};
}

const renderIndexList = (
props: Partial<React.ComponentProps<typeof RegularIndexesTable>> = {}
) => {
Expand Down Expand Up @@ -208,7 +194,7 @@ describe('RegularIndexesTable Component', function () {
});
});

describe.only('sorting', function () {
describe('sorting', function () {
function getIndexNames() {
return screen.getAllByTestId('indexes-name-field').map((el) => {
return el.textContent!.trim();
Expand All @@ -222,9 +208,9 @@ describe('RegularIndexesTable Component', function () {
it('sorts table by name', function () {
renderIndexList({
indexes: [
mockIndex({ name: 'b' }),
mockIndex({ name: 'a' }),
mockIndex({ name: 'c' }),
mockRegularIndex({ name: 'b' }),
mockRegularIndex({ name: 'a' }),
mockRegularIndex({ name: 'c' }),
],
});

Expand All @@ -240,9 +226,9 @@ describe('RegularIndexesTable Component', function () {
it('sorts table by type', function () {
renderIndexList({
indexes: [
mockIndex({ name: 'b' }),
mockIndex({ name: 'a' }),
mockIndex({ name: 'c' }),
mockRegularIndex({ name: 'b' }),
mockRegularIndex({ name: 'a' }),
mockRegularIndex({ name: 'c' }),
],
});

Expand All @@ -258,9 +244,9 @@ describe('RegularIndexesTable Component', function () {
it('sorts table by size', function () {
renderIndexList({
indexes: [
mockIndex({ name: 'b', size: 5 }),
mockIndex({ name: 'a', size: 1 }),
mockIndex({ name: 'c', size: 10 }),
mockRegularIndex({ name: 'b', size: 5 }),
mockRegularIndex({ name: 'a', size: 1 }),
mockRegularIndex({ name: 'c', size: 10 }),
],
});

Expand All @@ -276,9 +262,9 @@ describe('RegularIndexesTable Component', function () {
it('sorts table by usage', function () {
renderIndexList({
indexes: [
mockIndex({ name: 'b', usageCount: 5 }),
mockIndex({ name: 'a', usageCount: 0 }),
mockIndex({ name: 'c', usageCount: 10 }),
mockRegularIndex({ name: 'b', usageCount: 5 }),
mockRegularIndex({ name: 'a', usageCount: 0 }),
mockRegularIndex({ name: 'c', usageCount: 10 }),
],
});

Expand All @@ -294,9 +280,9 @@ describe('RegularIndexesTable Component', function () {
it('sorts table by properties', function () {
renderIndexList({
indexes: [
mockIndex({ name: 'b', properties: ['sparse'] }),
mockIndex({ name: 'a', properties: ['partial'] }),
mockIndex({
mockRegularIndex({ name: 'b', properties: ['sparse'] }),
mockRegularIndex({ name: 'a', properties: ['partial'] }),
mockRegularIndex({
name: 'c',
cardinality: 'compound',
properties: ['ttl'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,17 @@ import {
within,
waitFor,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { expect } from 'chai';
import sinon from 'sinon';
import type { Document } from 'mongodb';

import { SearchIndexesTable } from './search-indexes-table';
import { SearchIndexesStatuses } from '../../modules/search-indexes';
import {
searchIndexes as indexes,
vectorSearchIndexes,
} from './../../../test/fixtures/search-indexes';
import { mockSearchIndex } from '../../../test/helpers';

const renderIndexList = (
props: Partial<React.ComponentProps<typeof SearchIndexesTable>> = {}
Expand Down Expand Up @@ -200,4 +201,70 @@ describe('SearchIndexesTable Component', function () {
);
});
});

describe('sorting', function () {
function getIndexNames() {
return screen.getAllByTestId('search-indexes-name-field').map((el) => {
return el.textContent!.trim();
});
}

function clickSort(label: string) {
userEvent.click(screen.getByRole('button', { name: `Sort by ${label}` }));
}

it('sorts table by name', function () {
renderIndexList({
indexes: [
mockSearchIndex({ name: 'b' }),
mockSearchIndex({ name: 'a' }),
mockSearchIndex({ name: 'c' }),
],
});

expect(getIndexNames()).to.deep.eq(['b', 'a', 'c']);

clickSort('Name and Fields');
expect(getIndexNames()).to.deep.eq(['a', 'b', 'c']);

clickSort('Name and Fields');
expect(getIndexNames()).to.deep.eq(['c', 'b', 'a']);
});

it('sorts table by type', function () {
renderIndexList({
indexes: [
mockSearchIndex({ name: 'b', type: 'vector search' }),
mockSearchIndex({ name: 'a', type: 'search' }),
mockSearchIndex({ name: 'c', type: 'vector search' }),
],
});

expect(getIndexNames()).to.deep.eq(['b', 'a', 'c']);

clickSort('Name and Fields');
expect(getIndexNames()).to.deep.eq(['a', 'b', 'c']);

clickSort('Name and Fields');
expect(getIndexNames()).to.deep.eq(['c', 'b', 'a']);
});

it('sorts table by status', function () {
renderIndexList({
indexes: [
mockSearchIndex({ name: 'b', status: 'FAILED' }),
mockSearchIndex({ name: 'a', status: 'BUILDING' }),
mockSearchIndex({ name: 'c', status: 'READY' }),
],
});

expect(getIndexNames()).to.deep.eq(['b', 'a', 'c']);

clickSort('Name and Fields');
expect(getIndexNames()).to.deep.eq(['a', 'b', 'c']);

clickSort('Name and Fields');
expect(getIndexNames()).to.deep.eq(['c', 'b', 'a']);
});
});
});
30 changes: 30 additions & 0 deletions packages/compass-indexes/test/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { SearchIndex } from 'mongodb-data-service';
import type { RegularIndex } from '../src/modules/regular-indexes';

export function mockRegularIndex(info: Partial<RegularIndex>): RegularIndex {
return {
ns: 'test.test',
name: '_id_1',
key: {},
fields: [],
size: 0,
relativeSize: 0,
...info,
extra: {
...info.extra,
},
};
}

export function mockSearchIndex(info: Partial<SearchIndex>): SearchIndex {
return {
id: 'a',
name: 'test',
status: 'READY',
queryable: true,
...info,
latestDefinition: {
...info.latestDefinition,
},
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import React, {
useState,
} from 'react';
import type { ReactReduxContextValue, TypedUseSelectorHook } from 'react-redux';
import { Provider, createSelectorHook } from 'react-redux';
import { Provider, createSelectorHook, createStoreHook } from 'react-redux';
import type { AnyAction } from 'redux';
import { createStore } from 'redux';

Expand All @@ -19,31 +19,37 @@ const CLEANUP_TAB_STATE =

const RESET = 'compass-workspaces/workspace-tab-state-provider/RESET';

function createTabStore() {
return createStore(
(state: TabState = Object.create(null), action: AnyAction) => {
if (action.type === SET_STATE) {
return {
...state,
[action.tabId]: {
...state[action.tabId],
[action.stateId]: action.value,
},
};
}
if (action.type === CLEANUP_TAB_STATE) {
delete state[action.tabId];
return { ...state };
}
if (action.type === RESET) {
return Object.create(null);
}
return state;
}
);
}

type TabStateStore = ReturnType<typeof createTabStore>;

/**
* Exported for testing purposes only
* @internal
*/
export const tabStateStore = createStore(
(state: TabState = Object.create(null), action: AnyAction) => {
if (action.type === SET_STATE) {
return {
...state,
[action.tabId]: {
...state[action.tabId],
[action.stateId]: action.value,
},
};
}
if (action.type === CLEANUP_TAB_STATE) {
delete state[action.tabId];
return { ...state };
}
if (action.type === RESET) {
return Object.create(null);
}
return state;
}
);
export const tabStateStore = createTabStore();

export function cleanupTabState(tabId: string) {
tabStateStore.dispatch({ type: CLEANUP_TAB_STATE, tabId });
Expand Down Expand Up @@ -105,6 +111,8 @@ function useWorkspaceTabId() {
return tabId;
}

const useStore: () => TabStateStore = createStoreHook(TabStateStoreContext);

const useSelector: TypedUseSelectorHook<TabState> =
createSelectorHook(TabStateStoreContext);

Expand All @@ -117,7 +125,11 @@ function selectTabState<S>(state: TabState, tabId: string, key: string) {
/**
* useSelector but with a state fallback for testing environment
*/
function useTabStateSelector<S>(tabId: string, key: string): S {
function useTabStateSelector<S>(
tabId: string,
key: string,
store: TabStateStore
): S {
try {
return useSelector((state) => {
return selectTabState<S>(state, tabId, key);
Expand All @@ -135,11 +147,11 @@ function useTabStateSelector<S>(tabId: string, key: string): S {
/* eslint-disable react-hooks/rules-of-hooks */
const [, forceUpdate] = useState({});
useEffect(() => {
return tabStateStore.subscribe(() => {
return store.subscribe(() => {
forceUpdate({});
});
}, []);
return selectTabState(tabStateStore.getState(), tabId, key);
}, [store]);
return selectTabState(store.getState(), tabId, key);
/* eslint-enable react-hooks/rules-of-hooks */
}
throw err;
Expand All @@ -163,18 +175,40 @@ export function useTabState<S>(
): [S, SetState<S>] {
const keyRef = useRef(key);
const tabIdRef = useRef(useWorkspaceTabId());
const storeRef = useRef(
(() => {
try {
// eslint-disable-next-line react-hooks/rules-of-hooks
return useStore();
} catch (err) {
// This will throw when Redux provider is not available in the React
// context. In that case, if we are in the test environment we'll create
// a new store instance to make sure that state changes are not actually
// persisted between test suites but the tests are still able to run
if (
process.env.NODE_ENV === 'test' &&
/could not find react-redux context value/.test(
(err as Error).message
)
) {
return createTabStore();
}
throw err;
}
})()
);
const setState: SetState<S> = useCallback((newState) => {
const newVal =
typeof newState === 'function'
? (newState as (prevState: S) => S)(
selectTabState<S>(
tabStateStore.getState(),
storeRef.current.getState(),
tabIdRef.current,
keyRef.current
)
)
: newState;
tabStateStore.dispatch({
storeRef.current.dispatch({
type: SET_STATE,
tabId: tabIdRef.current,
stateId: keyRef.current,
Expand All @@ -186,7 +220,7 @@ export function useTabState<S>(
handledInitialState.current = true;
if (
!Object.prototype.hasOwnProperty.call(
tabStateStore.getState()[tabIdRef.current] ?? {},
storeRef.current.getState()[tabIdRef.current] ?? {},
keyRef.current
)
) {
Expand All @@ -198,6 +232,10 @@ export function useTabState<S>(
setState(initialState);
}
}
const state = useTabStateSelector<S>(tabIdRef.current, keyRef.current);
const state = useTabStateSelector<S>(
tabIdRef.current,
keyRef.current,
storeRef.current
);
return [state, setState];
}

0 comments on commit 560d55f

Please sign in to comment.