Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
icon="optionsHorizontal"
appearance="flat-button"
data-test="show-file-options"
:ariaLabel="$tr('fileOptionsButtonLabel')"
>
<template #menu>
<KDropdownMenu
Expand Down Expand Up @@ -218,6 +219,7 @@
replaceFileMenuOptionLabel: 'Replace file',
downloadMenuOptionLabel: 'Download',
removeMenuOptionLabel: 'Remove',
fileOptionsButtonLabel: 'File options',
downloadFailed: 'Failed to download file',
/* eslint-disable kolibri/vue-no-unused-translations */
removeFileButton: 'Remove',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
import { mount } from '@vue/test-utils';
import { render, screen, waitFor, configure } from '@testing-library/vue';
import userEvent from '@testing-library/user-event';
import FileUploadItem from '../FileUploadItem';
import { factory } from '../../../store';
import Uploader from 'shared/views/files/Uploader';
import { fileErrors } from 'shared/constants';
import { createTranslator } from 'shared/i18n';

configure({ testIdAttribute: 'data-test' });

jest.mock('shared/vuex/file/validation', () => ({
validateFile: jest.fn(() => Promise.resolve(0)),
}));

const tr = createTranslator('FileUploadItem', FileUploadItem.$trs);
const testFile = { id: 'test' };
function makeWrapper(props = {}, file = {}, computed = {}) {
const store = factory();
return mount(FileUploadItem, {

function renderComponent({ props = {}, file = {}, store = factory() } = {}) {
return render(FileUploadItem, {
routes: [],
store,
attachTo: document.body,
propsData: {
props: {
file:
file === null
? null
Expand All @@ -24,81 +34,162 @@ function makeWrapper(props = {}, file = {}, computed = {}) {
},
...props,
},
computed,
});
}

describe('fileUploadItem', () => {
describe('render', () => {

@rtibblesbot rtibblesbot Jul 17, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Resolved — addressed in the current code.

nitpick: The migration folds the interaction cases (opening the options menu, clicking the file row) under describe('render'), whereas the old file split them into a separate describe('methods') block. Harmless and arguably in keeping with VTL's behavior-first grouping — leave it, or rename this block to something like describe('file upload item').

it("'Unknown filename' should be displayed if original_filename is 'file'", () => {
const file = {
original_filename: 'file',
};
const wrapper = makeWrapper({}, file);
expect(wrapper.findComponent('[data-test="file-name"]').text()).toBe('Unknown filename');
it('shows "Unknown filename" when the uploaded file has a generic name', () => {
renderComponent({
file: {
original_filename: 'file',
},
});
expect(screen.getByText(tr.$tr('unknownFile'))).toBeInTheDocument();
});

it("'Unknown filename' should be displayed if original_filename is ''", () => {
const file = {
original_filename: '',
};
const wrapper = makeWrapper({}, file);
expect(wrapper.findComponent('[data-test="file-name"]').text()).toBe('Unknown filename');
it("shows 'Unknown filename' when the uploaded filename is ''", () => {
renderComponent({
file: {
original_filename: '',
},
});
expect(screen.getByText(tr.$tr('unknownFile'))).toBeInTheDocument();
});

it('shows the uploaded file name when it is available', () => {
renderComponent({
file: {
original_filename: 'SomeFileName',
},
});
expect(screen.getByText('SomeFileName')).toBeInTheDocument();
});

it("original_filename should be displayed if its value is not 'file'", () => {
const file = {
it('shows an upload error when the file upload failed', () => {
const store = factory();
store.commit('file/ADD_FILE', {
id: 'file-1',
original_filename: 'SomeFileName',
};
const wrapper = makeWrapper({}, file);
expect(wrapper.findComponent('[data-test="file-name"]').text()).toBe('SomeFileName');
preset: 'document',
checksum: 'checksum',
file_format: 'pdf',
loaded: 0,
total: 100,
error: fileErrors.UPLOAD_FAILED,
});
renderComponent({
store,
file: {
id: 'file-1',
original_filename: 'SomeFileName',
error: fileErrors.UPLOAD_FAILED,
},
});
expect(screen.getByText(tr.$tr('uploadFailed'))).toBeInTheDocument();
});

it('should show a status error if the file has an error', () => {
const wrapper = makeWrapper({}, { error: true });
expect(wrapper.findComponent('[data-test="status"]').exists()).toBe(true);
it('shows a Select file action when no file has been uploaded', () => {
renderComponent({
file: null,
});
expect(screen.getByText(tr.$tr('uploadButton'))).toBeInTheDocument();
});

it('should show an upload button if file is null', () => {
const wrapper = makeWrapper({}, null);
expect(wrapper.findComponent('[data-test="upload-link"]').exists()).toBe(true);
expect(wrapper.findComponent('[data-test="radio"]').exists()).toBe(false);
});
it('should show dropdown on click preview file options', async () => {
const wrapper = makeWrapper({ allowFileRemove: true });
await wrapper.findComponent('[data-test="show-file-options"]').trigger('click');
expect(wrapper.find('[data-test="file-options"]').isVisible()).toBe(true);
it('shows file actions when the user opens the options menu', async () => {
const user = userEvent.setup();
renderComponent({
props: {
allowFileRemove: true,
},
file: {
id: 'file-1',
original_filename: 'SomeFileName',
file_size: 100,
url: 'file-url',
},
});
await user.click(screen.getByRole('button', { name: tr.$tr('fileOptionsButtonLabel') }));
expect(screen.getByText(tr.$tr('replaceFileMenuOptionLabel'))).toBeInTheDocument();
expect(screen.getByText(tr.$tr('downloadMenuOptionLabel'))).toBeInTheDocument();
expect(screen.getByText(tr.$tr('removeMenuOptionLabel'))).toBeInTheDocument();
});
});

describe('methods', () => {
let wrapper;
it('calls the upload complete handler when the replacement upload finishes', async () => {
const store = factory();
store.commit('ADD_SESSION', { id: 1, disk_space: 209715200, disk_space_used: 0 });

beforeEach(() => {
wrapper = makeWrapper();
});

it('Uploader uploadCompleteHandler should call uploadCompleteHandler with file', async () => {
const file = {
const uploadCompleteHandler = jest.fn();
const fileObject = {
id: 'file-1',
preset: 'document',
checksum: 'checksum',
file_format: 'pdf',
original_filename: 'test.pdf',
loaded: 0,
total: 10,
};
const uploadCompleteHandler = jest.fn();
await wrapper.setProps({ uploadCompleteHandler });
await wrapper.setData({ fileUploadId: file.id });
wrapper.findComponent(Uploader).vm.uploadCompleteHandler(file);
expect(uploadCompleteHandler).toHaveBeenCalledWith(file);

const uploadFile = jest
.spyOn(Uploader.methods, 'uploadFile')

@rtibblesbot rtibblesbot Aug 11, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Resolved — addressed in the current code.

praise: Spying the vuex-action boundary and firing a real userEvent.upload runs the whole chain — handleFilesvalidateFiles → storage check → uploadingHandler → the id match in FileUploadItem.vue:194. The old stub short-circuited all of it.

.mockImplementation(async () => {
store.commit('file/ADD_FILE', fileObject);
return { fileObject, uploadPromise: Promise.resolve(fileObject) };
});

renderComponent({
store,
props: {
uploadCompleteHandler,
},
});

const fileInput = screen.getByTestId('upload-dialog');
await userEvent.upload(
fileInput,
new File(['pdf'], 'test.pdf', { type: 'application/pdf' }),
);

await waitFor(() => {
expect(uploadCompleteHandler).toHaveBeenCalledWith(
expect.objectContaining({
id: 'file-1',
}),
);
});

uploadFile.mockRestore();

@rtibblesbot rtibblesbot Aug 11, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Resolved — addressed in the current code.

suggestion: Restore in afterEach instead. This line and clickSpy.mockRestore() (192) only run if the test reaches its last line — if the waitFor above times out, Uploader.methods.uploadFile stays patched for the rest of the file and its mock keeps committing into the store from a finished test, turning one real failure into several confusing ones.

jest_config/jest.conf.js sets neither restoreMocks nor resetMocks, so a file-level afterEach(() => jest.restoreAllMocks()) covers both. The sibling spec does it this way — shared/views/files/__tests__/uploader.spec.js:33-37.

});

it('clicking a list item should emit a selected event if a file is available', async () => {
await wrapper.find('[data-test="list-item"]').trigger('click');
expect(wrapper.emitted('selected')).not.toBeUndefined();
it('selects the existing file when the user clicks the file row', async () => {
const user = userEvent.setup();
const { emitted } = renderComponent({
file: {
id: 'file-1',
original_filename: 'SomeFileName',
file_size: 100,
},
});

await user.click(screen.getByText('SomeFileName'));

expect(emitted().selected).toHaveLength(1);
});

it('clicking a list item should open the file dialog if file is not available', async () => {
wrapper = makeWrapper({}, null);
await wrapper.find('[data-test="list-item"]').trigger('click');
expect(wrapper.emitted('selected')).toBeUndefined();
it('opens the file chooser when the user clicks an empty file row', async () => {
const user = userEvent.setup();
const clickSpy = jest.spyOn(HTMLInputElement.prototype, 'click');

const { emitted } = renderComponent({
file: null,
});

await user.click(screen.getByText(tr.$tr('uploadButton')));

expect(clickSpy).toHaveBeenCalled();
expect(emitted()).not.toHaveProperty('selected');

clickSpy.mockRestore();
});
});
});