Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tests: add initial test suite for CoreMenu #11934

Merged
merged 12 commits into from
Apr 8, 2024
Merged
10 changes: 6 additions & 4 deletions kolibri/core/assets/src/views/CoreMenu/CoreMenuOption.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
@keydown.enter="visibleSubMenu = !visibleSubMenu"
>
<slot>
<KLabeledIcon :iconAfter="iconAfter">
<KLabeledIcon :iconAfter="iconAfter" :data-testid="`icon-${iconAfter}`">
<template v-if="icon" #icon>
<KIcon :icon="icon" :color="optionIconColor" />
<KIcon :icon="icon" :color="optionIconColor" :data-testid="`icon-${icon}`" />
</template>
<div v-if="label">{{ label }}</div>
</KLabeledIcon>
Expand All @@ -37,7 +37,7 @@
<slot>
<KLabeledIcon>
<template v-if="icon" #icon>
<KIcon :icon="icon" :color="optionIconColor" />
<KIcon :icon="icon" :color="optionIconColor" :data-testid="`icon-${icon}`" />
</template>
<div v-if="label">{{ label }}</div>
</KLabeledIcon>
Expand Down Expand Up @@ -110,7 +110,9 @@
subRoutes: {
type: Array,
required: false,
default: null,
default: () => [],
// subRoutes should be an array of objects with the name, label, and route properties
validate: subRoutes => subRoutes.every(route => route.name && route.label && route.route),
},
disabled: {
type: Boolean,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { render, screen } from '@testing-library/vue';
import VueRouter from 'vue-router';
import CoreMenuDivider from '../CoreMenuDivider.vue';

describe('CoreMenuDivider', () => {
test('renders the component', () => {
render(CoreMenuDivider, {
routes: new VueRouter(),
});

expect(screen.getByRole('listitem')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import { render, screen, fireEvent } from '@testing-library/vue';
import userEvent from '@testing-library/user-event';
import VueRouter from 'vue-router';
import CoreMenuOption from '../CoreMenuOption.vue';

const sampleSubRoutes = [
{ name: 'subRoute1', label: 'Sub Route 1' },
{ name: 'subRoute2', label: 'Sub Route 2' },
{ name: 'subRoute3', label: 'Sub Route 3' },
];
const sampleLink = 'https://mockurl.com/';
const sampleIcon = 'add';

const renderComponent = props => {
return render(CoreMenuOption, {
props: {
subRoutes: [],
...props,
},
routes: new VueRouter(),
});
};

describe('CoreMenuOption', () => {
test('smoke test', () => {
renderComponent();
expect(screen.getByRole('menuitem')).toBeInTheDocument();
});

describe('subRoutes toggle', () => {
EshaanAgg marked this conversation as resolved.
Show resolved Hide resolved
describe('when subRoutes are provided', () => {
it('should render with the chevronDown icon initially with the subroutes not visible', () => {
renderComponent({ subRoutes: sampleSubRoutes });

expect(screen.getByRole('menuitem')).toBeInTheDocument();
expect(screen.getByTestId('icon-chevronDown')).toBeInTheDocument();
sampleSubRoutes.forEach(subRoute =>
expect(screen.queryByText(subRoute.label)).not.toBeInTheDocument()
);
});

it('should open the submenu on clicking and the icons should change accordingly', async () => {
renderComponent({ subRoutes: sampleSubRoutes });
const menuItem = screen.getByRole('menuitem');

// Clicking should show the subroutes
await userEvent.click(menuItem);
sampleSubRoutes.forEach(subRoute =>
expect(screen.getByText(subRoute.label)).toBeInTheDocument()
);
expect(screen.getByTestId('icon-chevronUp')).toBeInTheDocument();
expect(screen.queryByTestId('icon-chevronDown')).not.toBeInTheDocument();
});

it('should close the submenu on clicking if it is open and the icons should change accordingly', async () => {
renderComponent({ subRoutes: sampleSubRoutes });
const menuItem = screen.getByRole('menuitem');

// Click to show the subroutes
await userEvent.click(menuItem);

// Clicking again should hide the subroutes
EshaanAgg marked this conversation as resolved.
Show resolved Hide resolved
await userEvent.click(menuItem);
sampleSubRoutes.forEach(subRoute =>
expect(screen.queryByText(subRoute.label)).not.toBeInTheDocument()
);
expect(screen.getByTestId('icon-chevronDown')).toBeInTheDocument();
expect(screen.queryByTestId('icon-chevronUp')).not.toBeInTheDocument();
});

it('should open the submenu on pressing Enter key and the icons should change accordingly', async () => {
renderComponent({ subRoutes: sampleSubRoutes });
// Pressing Enter should show the subroutes

await fireEvent.keyDown(screen.getByRole('menuitem'), { key: 'Enter' });
sampleSubRoutes.forEach(subRoute =>
expect(screen.getByText(subRoute.label)).toBeInTheDocument()
);
expect(screen.getByTestId('icon-chevronUp')).toBeInTheDocument();
expect(screen.queryByTestId('icon-chevronDown')).not.toBeInTheDocument();
});

it('should close the submenu on pressing Enter key if it is open and the icons should change accordingly', async () => {
renderComponent({ subRoutes: sampleSubRoutes });
const menuItem = screen.getByRole('menuitem');

// Pressing Enter to show the subroutes
await fireEvent.keyDown(menuItem, { key: 'Enter' });

// Pressing Enter again should hide the subroutes
await fireEvent.keyDown(menuItem, { key: 'Enter' });
sampleSubRoutes.forEach(subRoute =>
expect(screen.queryByText(subRoute.label)).not.toBeInTheDocument()
);
expect(screen.getByTestId('icon-chevronDown')).toBeInTheDocument();
expect(screen.queryByTestId('icon-chevronUp')).not.toBeInTheDocument();
});

it('pressing tab from keyboard should focus the menuitem', async () => {
renderComponent({ subRoutes: sampleSubRoutes });

await userEvent.tab();
expect(screen.getByRole('menuitem')).toHaveFocus();
});

it('should display the label of the option when provided', () => {
renderComponent({ label: 'Sample Option', subRoutes: sampleSubRoutes });
expect(screen.getByText('Sample Option')).toBeInTheDocument();
});

it('should display the secondary text of the option when provided', () => {
renderComponent({ secondaryText: 'Secondary Text', subRoutes: sampleSubRoutes });
expect(screen.getByText('Secondary Text')).toBeInTheDocument();
});

it('should display the icon of the option when provided', () => {
renderComponent({ icon: sampleIcon, subRoutes: sampleSubRoutes });
expect(screen.getByTestId(`icon-${sampleIcon}`)).toBeInTheDocument();
});
});

describe('when subRoutes are not provided', () => {
it('should render the menuitem with the provided URL', () => {
renderComponent({ subRoutes: [], link: sampleLink });

const menuItem = screen.getByRole('menuitem');
expect(menuItem).toBeInTheDocument();
expect(menuItem).toHaveAttribute('href', sampleLink);
});

it('should render the icon of the option when provided', () => {
renderComponent({
icon: sampleIcon,
subRoutes: [],
});
expect(screen.getByTestId(`icon-${sampleIcon}`)).toBeInTheDocument();
});

it('should display the label of the option when provided', () => {
renderComponent({ label: 'Sample Option', subRoutes: [] });
expect(screen.getByText('Sample Option')).toBeInTheDocument();
});

it('pressing tab from keyboard should focus the menuitem', async () => {
renderComponent({ subRoutes: [] });

// Press tab to focus the menuitem
await userEvent.tab();
expect(screen.getByRole('menuitem')).toHaveFocus();
});

describe('testing the user interactions', () => {
const testcases = [
{
name: 'should emit with link is not provided and is not disabled',
disabled: false,
link: null,
expected: true,
},
{
name: 'should not emit when disabled',
disabled: true,
link: null,
expected: false,
},
{
name: 'should not emit when link is provided',
disabled: false,
link: sampleLink,
expected: false,
},
];

test.each(testcases)('%s [Mouse Click]', async ({ disabled, link, expected }) => {
const { emitted } = renderComponent({ link, disabled, subRoutes: [] });

await userEvent.click(screen.getByRole('menuitem'));
if (expected) {
expect(emitted()).toHaveProperty('select');
expect(emitted().select).toHaveLength(1);
} else {
expect(emitted()).not.toHaveProperty('select');
}
});

test.each(testcases)('%s [Enter Key]', async ({ disabled, link, expected }) => {
const { emitted } = renderComponent({ link, disabled, subRoutes: [] });

await fireEvent.keyDown(screen.getByRole('menuitem'), { key: 'Enter' });
if (expected) {
expect(emitted()).toHaveProperty('select');
expect(emitted().select).toHaveLength(1);
} else {
expect(emitted()).not.toHaveProperty('select');
}
});
});
});
});
});
5 changes: 0 additions & 5 deletions packages/kolibri-tools/jest.conf/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,6 @@ Vue.config.productionTip = false;

i18nSetup(true);

const csrf = global.document.createElement('input');
csrf.name = 'csrfmiddlewaretoken';
csrf.value = 'csrfmiddlewaretoken';
global.document.body.append(csrf);

Object.defineProperty(window, 'scrollTo', { value: () => {}, writable: true });

// Shows better NodeJS unhandled promise rejection errors
Expand Down
13 changes: 7 additions & 6 deletions packages/kolibri-tools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
"@babel/plugin-syntax-import-assertions": "^7.24.1",
"@babel/plugin-transform-runtime": "^7.24.3",
"@babel/preset-env": "^7.24.3",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/user-event": "^14.5.2",
"@testing-library/vue": "^5",
"@vue/test-utils": "^1.3.6",
"ast-traverse": "^0.1.1",
"autoprefixer": "10.4.19",
Expand All @@ -42,8 +45,8 @@
"eslint-config-vue": "^2.0.2",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jest": "^23.3.0",
"eslint-plugin-kolibri": "0.16.1-dev.1",
"eslint-plugin-jest-dom": "^5.2.0",
"eslint-plugin-kolibri": "0.16.1-dev.1",
"eslint-plugin-vue": "^7.3.0",
"espree": "10.0.1",
"esquery": "^1.5.0",
Expand All @@ -67,6 +70,7 @@
"query-ast": "^1.0.5",
"readline-sync": "^1.4.9",
"recast": "^0.23.6",
"rewire": "^6.0.0",
"rtlcss": "4.1.1",
"sass-loader": "14.1.1",
"scss-parser": "^1.0.6",
Expand All @@ -81,7 +85,6 @@
"stylelint-config-standard": "24.0.0",
"stylelint-csstree-validator": "3.0.0",
"stylelint-scss": "5.3.2",
"rewire": "^6.0.0",
"temp": "^0.8.3",
"terser-webpack-plugin": "^5.3.10",
"toml": "^3.0.0",
Expand All @@ -93,12 +96,10 @@
"webpack": "^5.91.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.0.4",
"webpack-merge": "^5.10.0",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/vue": "^5"
"webpack-merge": "^5.10.0"
},
"engines": {
"node": "18.x",
"npm": ">= 8"
}
}
}
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1494,6 +1494,11 @@
lodash "^4.17.15"
redent "^3.0.0"

"@testing-library/user-event@^14.5.2":
version "14.5.2"
resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.5.2.tgz#db7257d727c891905947bd1c1a99da20e03c2ebd"
integrity sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==

"@testing-library/vue@^5":
version "5.9.0"
resolved "https://registry.yarnpkg.com/@testing-library/vue/-/vue-5.9.0.tgz#d33c52ae89e076808abe622f70dcbccb1b5d080c"
Expand Down