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 tests for some components #12108

Merged
merged 10 commits into from
May 3, 2024
2 changes: 1 addition & 1 deletion kolibri/core/assets/src/views/TimeDuration.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>

<KOptionalText
:text="seconds ? formattedTime : ''"
:text="seconds !== null ? formattedTime : ''"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made this change as earlier passing 0 as seconds led to this expression being evaluated as false, and thus an empty string being displayed instead of 0 seconds. Also added a test case related to the same.

/>

</template>
Expand Down
82 changes: 82 additions & 0 deletions kolibri/core/assets/src/views/__tests__/FocusTrap.spec.js
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not very sure if the way I have chosen is the most user-centric way to test this FocusTrap component. Would appreciate any feedback on the same.

Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { render, screen, fireEvent } from '@testing-library/vue';

Check failure on line 1 in kolibri/core/assets/src/views/__tests__/FocusTrap.spec.js

View workflow job for this annotation

GitHub Actions / All file linting

'screen' is defined but never used

Check failure on line 1 in kolibri/core/assets/src/views/__tests__/FocusTrap.spec.js

View workflow job for this annotation

GitHub Actions / All file linting

'fireEvent' is defined but never used
import userEvent from '@testing-library/user-event';
import FocusTrap from '../FocusTrap.vue';
import FocusTrapWrapper from './FocusTrapWrapper.vue';

Check failure on line 4 in kolibri/core/assets/src/views/__tests__/FocusTrap.spec.js

View workflow job for this annotation

GitHub Actions / All file linting

'FocusTrapWrapper' is defined but never used

const renderComponent = (props = {}) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The FocusTrap tests look good.

Not a blocker -- the only thing I could think that might be worth adding is a call to the public reset method on FocusTrap to see that it updates isTrapActive and such.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it! That's an important use case. Will add tests for it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added a test suite to cover this use-case, but I can't get it to work. Like to access the public method reset of the component, we would need to add a wrapper around it so that the user can access the same. I tried writing a test suite with the same, but can't get it to work. Would highly appreciate any help with the same.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a case where the 'user' of the component is actually the developer, so I don't think it's necessary to do this via DOM interaction, and the reset method can be invoked programmatically - as it forms part of the public API of the component.

return render(FocusTrap, {
props: {
disabled: false,
...props,
},
});
};

describe('FocusTrap', () => {
it('should emit the "shouldFocusFirstEl" element when the tab key is pressed once', async () => {
const { emitted } = renderComponent();

await userEvent.tab();
expect(emitted()).toHaveProperty('shouldFocusFirstEl');
expect(emitted().shouldFocusFirstEl.length).toBe(1);
});

it("should trap the focus and emit 'shouldFocusFirstEl' if the last focusable element is focused and we focus the next element", async () => {
const { emitted } = renderComponent();

await userEvent.tab();
await userEvent.tab();

expect(emitted()).toHaveProperty('shouldFocusFirstEl');
expect(emitted().shouldFocusFirstEl.length).toBe(2);
});

it('should trap the focus and emit "shouldFocusLastEl" when the first element is focused and we focus the previous element', async () => {
const { emitted } = renderComponent();

await userEvent.tab();
await userEvent.tab();

// Shift + Tab is used to focus on the initial element again
await userEvent.tab({ shift: true });

expect(emitted()).toHaveProperty('shouldFocusLastEl');
expect(emitted().shouldFocusLastEl.length).toBe(1);
});

it("should not trap focus when 'disabled' prop is set to true", async () => {
const { emitted } = renderComponent({ disabled: true });

await userEvent.tab();
expect(emitted()).not.toHaveProperty('shouldFocusFirstEl');

await userEvent.tab();
expect(emitted()).not.toHaveProperty('shouldFocusFirstEl');

await userEvent.tab({ shift: true });
expect(emitted()).not.toHaveProperty('shouldFocusLastEl');
});

// it("should reset state when 'reset' method is called", async () => {
// // FocusTrapWrapper is used to test the FocusTrap component's reset method
// // It has a button which calls the reset method of the FocusTrap component
// const { emitted } = render(FocusTrapWrapper);

// await fireEvent.focus(screen.getByTestId('focusTrap'));
// // Activate the focus trap
// await userEvent.tab();
// await userEvent.tab();
// await userEvent.tab({ shift: true });

// // The focus trap should be active
// expect(emitted()).toHaveProperty('shouldFocusLastEl');
// expect(emitted().shouldFocusLastEl.length).toBe(1);

// // Reset the focus trap
// await userEvent.click(screen.getByRole('button'));

// await userEvent.tab();
// expect(emitted()).toHaveProperty('shouldFocusFirstEl');
// expect(emitted().shouldFocusFirstEl.length).toBe(2);
// });
});
29 changes: 29 additions & 0 deletions kolibri/core/assets/src/views/__tests__/FocusTrapWrapper.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<!-- This is a wrapper around the FocusTrap component
to test it's exported public "reset" method -->
<template>

<FocusTrap ref="focusTrap" data-testid="focusTrap" :disabled="false">
<button @click="reset">
Reset
</button>
</FocusTrap>

</template>

<script>

import FocusTrap from '../FocusTrap.vue';

export default {
name: 'FocusTrapWrapper',
components: {
FocusTrap,
},
methods: {
reset() {
this.$refs.focusTrap.reset();
},
},
};

</script>
53 changes: 53 additions & 0 deletions kolibri/core/assets/src/views/__tests__/TimeDuration.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { render, screen } from '@testing-library/vue';
import TimeDuration from '../TimeDuration.vue';

const renderComponent = (props = {}) => {
return render(TimeDuration, {
props,
});
};

const MINUTE = 60;
const HOUR = 60 * MINUTE;
const DAY = 24 * HOUR;

const testCases = [
// Under 2 minutes, should show seconds
{ seconds: 0, expected: '0 seconds' },
EshaanAgg marked this conversation as resolved.
Show resolved Hide resolved
{ seconds: 1, expected: '1 second' },
{ seconds: 59, expected: '59 seconds' },
{ seconds: MINUTE, expected: '60 seconds' },
{ seconds: 2 * MINUTE - 1, expected: '119 seconds' },

// Under 1 hour, should show minutes (rounded down)
{ seconds: 2 * MINUTE, expected: '2 minutes' },
{ seconds: 30 * MINUTE, expected: '30 minutes' },
{ seconds: 30 * MINUTE + 1, expected: '30 minutes' },
{ seconds: 30 * MINUTE + 59, expected: '30 minutes' },
{ seconds: 59 * MINUTE, expected: '59 minutes' },

// Under 1 day, should show hours (rounded down)
{ seconds: HOUR, expected: '1 hour' },
{ seconds: 2 * HOUR, expected: '2 hours' },
{ seconds: 23 * HOUR, expected: '23 hours' },
{ seconds: 23 * HOUR + 59 * MINUTE, expected: '23 hours' },
{ seconds: 23 * HOUR + 59 * MINUTE + 59, expected: '23 hours' },

// Over 1 day, should show days (rounded down)
{ seconds: DAY, expected: '1 day' },
{ seconds: 2 * DAY, expected: '2 days' },
{ seconds: 6 * DAY, expected: '6 days' },
{ seconds: 6 * DAY + 23 * HOUR + 59 * MINUTE + 59, expected: '6 days' },
];

describe('TimeDuration', () => {
it.each(testCases)('should render $seconds seconds as $expected', ({ seconds, expected }) => {
renderComponent({ seconds });
expect(screen.getByText(expected)).toBeInTheDocument();
});

it('should render empty string if seconds are not provided as props', () => {
renderComponent();
expect(screen.getByText('—')).toBeInTheDocument();
});
});
43 changes: 43 additions & 0 deletions kolibri/core/assets/src/views/__tests__/UserTypeDisplay.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { render, screen } from '@testing-library/vue';
import UserTypeDisplay from '../UserTypeDisplay.vue';

const sampleUserType = 'testing-user-type';
const expectedSampleUserType = 'Testing User Type';

// Helper function to render the component with the provided props
const renderComponent = props => {
const translatedUserKinds = {
computed: {
typeDisplayMap() {
return {
[sampleUserType]: expectedSampleUserType,
};
},
},
};

return render(UserTypeDisplay, {
props: {
userType: sampleUserType,
...props,
},
mixins: [translatedUserKinds],
});
};

describe('UserTypeDisplay', () => {
test('smoke test (renders the translated user type correctly)', () => {
renderComponent({ userType: sampleUserType });
expect(screen.getByText(expectedSampleUserType)).toBeInTheDocument();
});

test('does not render the untranslated user type', () => {
renderComponent({ userType: sampleUserType });
expect(screen.queryByText(sampleUserType)).not.toBeInTheDocument();
});

test('does not render anything if the userType prop is not provided', () => {
const { container } = renderComponent({ userType: undefined });
expect(container).toBeEmptyDOMElement();
});
});
Loading