Skip to content

Commit

Permalink
fix test util issues
Browse files Browse the repository at this point in the history
  • Loading branch information
matttdawson committed Feb 2, 2025
1 parent 7c9d811 commit 586705f
Show file tree
Hide file tree
Showing 8 changed files with 304 additions and 4 deletions.
6 changes: 5 additions & 1 deletion rollup.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ module.exports = {
resolve({ preferBuiltins: true }),
commonjs(),
typescript({
exclude: ['src/stories/**/*.*'],
exclude: ['src/stories/**/*.*', 'src/**/__tests__/*.*'],
}),
postcss(postcssOptions()),
json(),
Expand All @@ -57,6 +57,10 @@ module.exports = {
src: 'src/styles/GridTheme.scss',
dest: `${outputDir}`,
},
{
src: 'src/utils/__tests__/*.ts',
dest: `${outputDir}/src/utils/__tests__/`,
},
],
}),
],
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@ export * from './react-menu3/index';
export * from './react-menu3/types';
export * from './utils/bearing';
export * from './utils/deferredPromise';
export * from './utils/textMatcher';
export * from './utils/textValidator';
export * from './utils/util';
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/
import { IconName } from '@linzjs/lui/dist/components/LuiIcon/LuiIcon';

import { wait } from './util';
import { wait } from '../util';

const queryAllBySelector = <T extends HTMLElement>(selector: string, container: HTMLElement = document.body): T[] =>
Array.from(container.querySelectorAll<T>(selector));
Expand Down
File renamed without changes.
295 changes: 295 additions & 0 deletions src/utils/__tests__/vitestUtil.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
import { act, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { isEqual } from 'lodash-es';
import { expect } from 'vitest';

import { findQuick, getAllQuick, getMatcher, getQuick, IQueryQuick, queryQuick } from './testQuick';

let user = userEvent;
/**
* allow external userEvent to be used
* @param customisedUserEvent
*/
export const setUpUserEvent = (customisedUserEvent: any) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
user = customisedUserEvent;
};

export const countRows = (within?: HTMLElement): number => {
return getAllQuick({ tagName: `div[row-id]:not(:empty)` }, within).length;
};

export const findRowByIndex = async (rowIndex: number | string, within?: HTMLElement): Promise<HTMLDivElement> => {
await waitFor(() => {
expect(getAllQuick({ classes: '.ag-row' }).length > 0).toBe(true);
});
//if this is not wrapped in an act console errors are logged during testing
let row!: HTMLDivElement;
await act(async () => {
row = await findQuick<HTMLDivElement>(
{ tagName: `.ag-center-cols-container div[row-index='${rowIndex}']:not(:empty)` },
within,
);
let combineChildren = [...Array.from(row.children)];

const leftCols = queryQuick<HTMLDivElement>(
{ tagName: `.ag-pinned-left-cols-container div[row-index='${rowIndex}']` },
within,
);
if (leftCols) {
combineChildren = [...Array.from(leftCols.children), ...combineChildren];
}

const rightCols = queryQuick<HTMLDivElement>(
{ tagName: `.ag-pinned-right-cols-container div[row-index='${rowIndex}']` },
within,
);
if (rightCols) {
combineChildren = [...Array.from(rightCols.children), ...combineChildren];
}

row.replaceChildren(...combineChildren);
});
return row;
};

export const findRow = async (rowId: number | string, within?: HTMLElement): Promise<HTMLDivElement> => {
await waitFor(() => {
expect(getAllQuick({ classes: '.ag-row' }).length > 0).toBe(true);
});
//if this is not wrapped in an act console errors are logged during testing
let row!: HTMLDivElement;
await act(async () => {
row = await findQuick<HTMLDivElement>(
{ tagName: `.ag-center-cols-container div[row-id='${rowId}']:not(:empty)` },
within,
);
let combineChildren = [...Array.from(row.children)];

const leftCols = queryQuick<HTMLDivElement>(
{ tagName: `.ag-pinned-left-cols-container div[row-id='${rowId}']` },
within,
);
if (leftCols) {
combineChildren = [...Array.from(leftCols.children), ...combineChildren];
}

const rightCols = queryQuick<HTMLDivElement>(
{ tagName: `.ag-pinned-right-cols-container div[row-id='${rowId}']` },
within,
);
if (rightCols) {
combineChildren = [...Array.from(rightCols.children), ...combineChildren];
}

row.replaceChildren(...combineChildren);
});
return row;
};

export const queryRow = (rowId: number | string, within?: HTMLElement): HTMLDivElement | null => {
return queryQuick<HTMLDivElement>({ tagName: `div[row-id='${rowId}']:not(:empty)` }, within);
};

const _selectRow = async (
select: 'select' | 'deselect' | 'toggle',
rowId: string | number,
within?: HTMLElement,
): Promise<void> => {
const row = await findRow(rowId, within);
const isSelected = row.className.includes('ag-row-selected');
if (select === 'toggle' || (select === 'select' && !isSelected) || (select === 'deselect' && isSelected)) {
const cell = await findCell(rowId, 'selection', within);
await user.click(cell);
await waitFor(async () => {
const row = await findRow(rowId, within);
const nowSelected = row.className.includes('ag-row-selected');
if (nowSelected === isSelected) throw `Row ${rowId} won't select`;
});
}
};

export const selectRow = async (rowId: string | number, within?: HTMLElement): Promise<void> =>
_selectRow('select', rowId, within);

export const deselectRow = async (rowId: string | number, within?: HTMLElement): Promise<void> =>
_selectRow('deselect', rowId, within);

export const findCell = async (rowId: number | string, colId: string, within?: HTMLElement): Promise<HTMLElement> => {
const row = await findRow(rowId, within);
return await findQuick({ tagName: `[col-id='${colId}']` }, row);
};

export const findCellContains = async (
rowId: number | string,
colId: string,
text: string | RegExp,
within?: HTMLElement,
) => {
return await waitFor(
async () => {
const row = await findRow(rowId, within);
return getQuick({ tagName: `[col-id='${colId}']`, text }, row);
},
{ timeout: 10000 },
);
};

export const selectCell = async (rowId: string | number, colId: string, within?: HTMLElement): Promise<void> => {
const cell = await findCell(rowId, colId, within);
await user.click(cell);
};

export const editCell = async (rowId: number | string, colId: string, within?: HTMLElement): Promise<void> => {
await waitFor(
async () => {
const cell = await findCell(rowId, colId, within);
await user.dblClick(cell);
await waitFor(findOpenPopover, { timeout: 1000 });
},
{ timeout: 10000 },
);
};

export const isCellReadOnly = async (rowId: number | string, colId: string, within?: HTMLElement): Promise<boolean> => {
const cell = await findCell(rowId, colId, within);
return cell.className.includes('GridCell-readonly');
};

export const findOpenPopover = () => findQuick({ classes: '.szh-menu--state-open' });

export const queryMenuOption = async (menuOptionText: string | RegExp): Promise<HTMLElement | null> => {
const openMenu = await findOpenPopover();
const els = await within(openMenu).findAllByRole('menuitem');
const matcher = getMatcher(menuOptionText);
const result = els.find(matcher);
return result ?? null;
};

export const findMenuOption = async (menuOptionText: string | RegExp): Promise<HTMLElement> => {
return await waitFor(
async () => {
const menuOption = await queryMenuOption(menuOptionText);
if (menuOption == null) {
throw Error(`Unable to find menu option ${menuOptionText}`);
}
return menuOption;
},
{ timeout: 5000 },
);
};

export const validateMenuOptions = async (
rowId: number | string,
colId: string,
expectedMenuOptions: Array<string>,
): Promise<boolean> => {
await editCell(rowId, colId);
const openMenu = await findOpenPopover();
const actualOptions = (await within(openMenu).findAllByRole('menuitem')).map((menuItem) => menuItem.textContent);
return isEqual(actualOptions, expectedMenuOptions);
};

export const clickMenuOption = async (menuOptionText: string | RegExp): Promise<void> => {
const menuOption = await findMenuOption(menuOptionText);
await user.click(menuOption);
};

export const openAndClickMenuOption = async (
rowId: number | string,
colId: string,
menuOptionText: string | RegExp,
within?: HTMLElement,
): Promise<void> => {
await editCell(rowId, colId, within);
await clickMenuOption(menuOptionText);
};

export const openAndFindMenuOption = async (
rowId: number | string,
colId: string,
menuOptionText: string | RegExp,
within?: HTMLElement,
): Promise<HTMLElement> => {
await editCell(rowId, colId, within);
return await findMenuOption(menuOptionText);
};

export const getMultiSelectOptions = async () => {
const openMenu = await findOpenPopover();
return getAllQuick<HTMLInputElement>({ role: 'menuitem', child: { tagName: 'input,textarea' } }, openMenu).map(
(input) => {
return {
v: input.value,
c: input.checked ?? true,
};
},
);
};

export const findMultiSelectOption = async (value: string): Promise<HTMLElement> => {
const openMenu = await findOpenPopover();
return getQuick({ role: 'menuitem', child: { tagName: `input[value='${value}']` } }, openMenu);
};

export const clickMultiSelectOption = async (value: string): Promise<void> => {
const menuItem = await findMultiSelectOption(value);
menuItem.parentElement && (await user.click(menuItem.parentElement));
};

const typeInput = async (value: string, filter: IQueryQuick): Promise<void> => {
const openMenu = await findOpenPopover();
const input = await findQuick(filter, openMenu);
await user.clear(input);
//'typing' an empty string will cause a console error, and it's also unnecessary after the previous clear call
if (value.length > 0) {
await user.type(input, value);
}
};

export const typeOnlyInput = async (value: string): Promise<void> =>
typeInput(value, { child: { tagName: "input[type='text'], textarea" } });

export const typeInputByLabel = async (value: string, labelText: string): Promise<void> => {
const labels = getAllQuick({ child: { tagName: 'label' } }).filter((l) => l.textContent === labelText);
if (labels.length === 0) {
throw Error(`Label not found for text: ${labelText}`);
}
if (labels.length > 1) {
throw Error(`Multiple labels found for text: ${labelText}`);
}
const inputId = labels[0].getAttribute('for');
await typeInput(value, { child: { tagName: `input[id='${inputId}'], textarea[id='${inputId}']` } });
};

export const typeInputByPlaceholder = async (value: string, placeholder: string): Promise<void> =>
typeInput(value, {
child: { tagName: `input[placeholder='${placeholder}'], textarea[placeholder='${placeholder}']` },
});

export const typeOtherInput = async (value: string): Promise<void> =>
typeInput(value, { classes: '.subComponent', child: { tagName: "input[type='text']" } });

export const typeOtherTextArea = async (value: string): Promise<void> =>
typeInput(value, { classes: '.subComponent', child: { tagName: 'textarea' } });

export const closeMenu = () => user.click(document.body);
export const closePopover = () => user.click(document.body);

export const findActionButton = (text: string, container?: HTMLElement): Promise<HTMLElement> =>
findQuick({ tagName: 'button', child: { classes: '.ActionButton-minimalAreaDisplay', text: text } }, container);

export const clickActionButton = async (text: string, container?: HTMLElement): Promise<void> => {
const button = await findActionButton(text, container);
await user.click(button);
};

export const waitForGridReady = async (props?: { grid?: HTMLElement; timeout?: number }) =>
waitFor(() => expect(getAllQuick({ classes: '.Grid-ready' }, props?.grid)).toBeDefined(), {
timeout: props?.timeout ?? 5000,
});

export const waitForGridRows = async (props?: { grid?: HTMLElement; timeout?: number }) =>
waitFor(() => expect(getAllQuick({ classes: '.ag-row' }, props?.grid).length > 0).toBe(true), {
timeout: props?.timeout ?? 5000,
});
1 change: 0 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
// see https://www.typescriptlang.org/tsconfig to better understand tsconfigs
"include": ["src", "./vite.config.ts"],
"exclude": ["**/testUtils.ts"],
"compilerOptions": {
"baseUrl": "./src",
"module": "esnext",
Expand Down
2 changes: 1 addition & 1 deletion vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { defineConfig, UserConfig } from 'vite';
import { createHtmlPlugin } from 'vite-plugin-html';
import tsconfigPaths from 'vite-tsconfig-paths';

export default defineConfig(async (): Promise<UserConfig> => {
export default defineConfig((): UserConfig => {
return {
envDir: 'app-env',
plugins: [react(), tsconfigPaths(), createHtmlPlugin()],
Expand Down

0 comments on commit 586705f

Please sign in to comment.