@@ -363,5 +392,20 @@
document.querySelector('[data-glpi-knowbase-side-panel]'),
document.querySelector('[data-glpi-knowbase-side-panel-offcanvas]'),
);
+
+ // Initialize document upload controller when modal opens
+ const uploadModal = document.getElementById('kb-add-document-modal');
+ if (uploadModal) {
+ uploadModal.addEventListener('shown.bs.modal', async () => {
+ const uploadPane = uploadModal.querySelector('#kb-modal-upload-pane');
+ if (uploadPane && !uploadPane.dataset.initialized) {
+ const uploadModule = await import(
+ "/js/modules/Knowbase/UploadController.js"
+ );
+ new uploadModule.DocumentUploadController(uploadPane, uploadModal);
+ uploadPane.dataset.initialized = 'true';
+ }
+ }, { once: true });
+ }
})();
diff --git a/tests/e2e/fixtures/uploads/test.json b/tests/e2e/fixtures/uploads/test.json
new file mode 100644
index 00000000000..ff517772167
--- /dev/null
+++ b/tests/e2e/fixtures/uploads/test.json
@@ -0,0 +1 @@
+{"test": true}
diff --git a/tests/e2e/pages/KnowbaseItemPage.ts b/tests/e2e/pages/KnowbaseItemPage.ts
index 062cb1c0673..5d05dc5e392 100644
--- a/tests/e2e/pages/KnowbaseItemPage.ts
+++ b/tests/e2e/pages/KnowbaseItemPage.ts
@@ -30,7 +30,8 @@
* ---------------------------------------------------------------------
*/
-import { Locator, Page } from "@playwright/test";
+import { expect, Locator, Page } from "@playwright/test";
+import path from 'path';
import { GlpiPage } from "./GlpiPage";
import { TipTapEditorHelper } from "../utils/TipTapEditorHelper";
import { SlashMenuHelper } from "../utils/SlashMenuHelper";
@@ -80,7 +81,8 @@ export class KnowbaseItemPage extends GlpiPage
public async goto(id: number): Promise
{
await this.page.goto(
- `/front/knowbaseitem.form.php?id=${id}&forcetab=KnowbaseItem$1`
+ `/front/knowbaseitem.form.php?id=${id}&forcetab=KnowbaseItem$1`,
+ { waitUntil: 'domcontentloaded' }
);
}
@@ -128,4 +130,39 @@ export class KnowbaseItemPage extends GlpiPage
{
return this.page.getByPlaceholder("Add a comment...");
}
+
+ public async doSelectFilesForKbUpload(files: string[], modal: Locator): Promise
+ {
+ const filePaths = files.map(file => path.join(__dirname, `../../fixtures/${file}`));
+
+ // Use filechooser event - click the label to trigger the hidden file input
+ const fileChooserPromise = this.page.waitForEvent('filechooser');
+ await modal.getByText('Drop files here or click to browse').click();
+ const fileChooser = await fileChooserPromise;
+ await fileChooser.setFiles(filePaths);
+
+ // Wait for files to be processed and appear in preview
+ await expect(modal.getByRole('listitem')).toHaveCount(files.length);
+
+ // Wait for all uploads to tmp to complete (button becomes enabled)
+ await expect(modal.getByRole('button', { name: 'Upload Documents' })).toBeEnabled();
+ }
+
+ public async doAddFileToKbUploadArea(file: string, modal: Locator): Promise
+ {
+ await this.doSelectFilesForKbUpload([file], modal);
+ await modal.getByRole('button', { name: 'Upload Documents' }).click();
+ await expect(modal).toBeHidden();
+ // Wait for page reload after upload
+ await this.page.waitForLoadState('load');
+ }
+
+ public async doAddFilesToKbUploadArea(files: string[], modal: Locator): Promise
+ {
+ await this.doSelectFilesForKbUpload(files, modal);
+ await modal.getByRole('button', { name: 'Upload Documents' }).click();
+ await expect(modal).toBeHidden();
+ // Wait for page reload after upload
+ await this.page.waitForLoadState('load');
+ }
}
diff --git a/tests/e2e/specs/Knowbase/document_upload.spec.ts b/tests/e2e/specs/Knowbase/document_upload.spec.ts
new file mode 100644
index 00000000000..7d67d8660d0
--- /dev/null
+++ b/tests/e2e/specs/Knowbase/document_upload.spec.ts
@@ -0,0 +1,256 @@
+/**
+ * ---------------------------------------------------------------------
+ *
+ * GLPI - Gestionnaire Libre de Parc Informatique
+ *
+ * http://glpi-project.org
+ *
+ * @copyright 2015-2026 Teclib' and contributors.
+ * @licence https://www.gnu.org/licenses/gpl-3.0.html
+ *
+ * ---------------------------------------------------------------------
+ *
+ * LICENSE
+ *
+ * This file is part of GLPI.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * ---------------------------------------------------------------------
+ */
+
+import { expect, test } from "../../fixtures/glpi_fixture";
+import { KnowbaseItemPage } from "../../pages/KnowbaseItemPage";
+import { Profiles } from "../../utils/Profiles";
+import { getWorkerEntityId } from "../../utils/WorkerEntities";
+import path from 'path';
+
+test('Can open document upload modal', async ({ page, profile, api }) => {
+ await profile.set(Profiles.SuperAdmin);
+ const kb = new KnowbaseItemPage(page);
+
+ const id = await api.createItem('KnowbaseItem', {
+ name: 'KB entry for document upload test',
+ entities_id: getWorkerEntityId(),
+ answer: "Article content",
+ });
+
+ await kb.goto(id);
+
+ // Open the document upload modal
+ await page.getByRole('button', { name: 'Add Document' }).click();
+ const modal = page.getByRole('dialog');
+ await expect(modal).toBeVisible();
+
+ // Verify both tabs are visible
+ await expect(modal.getByRole('tab', { name: 'Upload a file' })).toBeVisible();
+ await expect(modal.getByRole('tab', { name: 'Link a document' })).toBeVisible();
+
+ // Verify the upload tab is active by default
+ await expect(modal.getByRole('tab', { name: 'Upload a file' })).toHaveAttribute('aria-selected', 'true');
+});
+
+test('Can select files and upload via modal', async ({ page, profile, api }) => {
+ await profile.set(Profiles.SuperAdmin);
+ const kb = new KnowbaseItemPage(page);
+
+ const id = await api.createItem('KnowbaseItem', {
+ name: 'KB entry for document upload test',
+ entities_id: getWorkerEntityId(),
+ answer: "Article content",
+ });
+
+ await kb.goto(id);
+
+ // Open the document upload modal
+ await page.getByRole('button', { name: 'Add Document' }).click();
+ const modal = page.getByRole('dialog');
+ await expect(modal).toBeVisible();
+
+ // Select a file - verify it appears in preview
+ await kb.doSelectFilesForKbUpload(['uploads/foo.png'], modal);
+
+ // Verify file is shown in preview
+ await expect(modal.getByRole('listitem')).toHaveCount(1);
+ await expect(modal.getByRole('listitem')).toContainText('foo.png');
+
+ // Verify upload button is enabled
+ await expect(modal.getByRole('button', { name: 'Upload Documents' })).toBeEnabled();
+
+ // Click upload and verify modal closes
+ await modal.getByRole('button', { name: 'Upload Documents' }).click();
+ await expect(modal).toBeHidden();
+
+ // Wait for page reload
+ await page.waitForLoadState('load');
+});
+
+test('Can select multiple files for upload', async ({ page, profile, api }) => {
+ await profile.set(Profiles.SuperAdmin);
+ const kb = new KnowbaseItemPage(page);
+
+ const id = await api.createItem('KnowbaseItem', {
+ name: 'KB entry for multiple document upload test',
+ entities_id: getWorkerEntityId(),
+ answer: "Article content",
+ });
+
+ await kb.goto(id);
+
+ // Open the document upload modal
+ await page.getByRole('button', { name: 'Add Document' }).click();
+ const modal = page.getByRole('dialog');
+ await expect(modal).toBeVisible();
+
+ // Select multiple files
+ await kb.doSelectFilesForKbUpload(['uploads/foo.png', 'uploads/bar.png'], modal);
+
+ // Verify both files are shown in preview
+ await expect(modal.getByRole('listitem')).toHaveCount(2);
+
+ // Upload and verify modal closes
+ await modal.getByRole('button', { name: 'Upload Documents' }).click();
+ await expect(modal).toBeHidden();
+
+ // Wait for page reload
+ await page.waitForLoadState('load');
+});
+
+test('Can remove a file from selection', async ({ page, profile, api }) => {
+ await profile.set(Profiles.SuperAdmin);
+ const kb = new KnowbaseItemPage(page);
+
+ const id = await api.createItem('KnowbaseItem', {
+ name: 'KB entry for file removal test',
+ entities_id: getWorkerEntityId(),
+ answer: "Article content",
+ });
+
+ await kb.goto(id);
+
+ // Open the document upload modal
+ await page.getByRole('button', { name: 'Add Document' }).click();
+ const modal = page.getByRole('dialog');
+ await expect(modal).toBeVisible();
+
+ // Select multiple files
+ await kb.doSelectFilesForKbUpload(['uploads/foo.png', 'uploads/bar.png'], modal);
+
+ // Verify files are in the preview
+ const fileItems = modal.getByRole('listitem');
+ await expect(fileItems).toHaveCount(2);
+
+ // Remove the first file
+ await fileItems.first().getByTitle('Remove').click();
+
+ // Verify only one file remains
+ await expect(fileItems).toHaveCount(1);
+});
+
+test('Can add description before upload', async ({ page, profile, api }) => {
+ await profile.set(Profiles.SuperAdmin);
+ const kb = new KnowbaseItemPage(page);
+
+ const id = await api.createItem('KnowbaseItem', {
+ name: 'KB entry for document description test',
+ entities_id: getWorkerEntityId(),
+ answer: "Article content",
+ });
+
+ await kb.goto(id);
+
+ // Open the document upload modal
+ await page.getByRole('button', { name: 'Add Document' }).click();
+ const modal = page.getByRole('dialog');
+ await expect(modal).toBeVisible();
+
+ // Select a file
+ await kb.doSelectFilesForKbUpload(['uploads/foo.png'], modal);
+
+ // Fill the description field
+ const description = 'Test document description';
+ await modal.getByLabel('Description').fill(description);
+
+ // Verify description is filled
+ await expect(modal.getByLabel('Description')).toHaveValue(description);
+
+ // Upload the file
+ await modal.getByRole('button', { name: 'Upload Documents' }).click();
+ await expect(modal).toBeHidden();
+
+ // Wait for page reload
+ await page.waitForLoadState('load');
+});
+
+test('Shows error for disallowed file type', async ({ page, profile, api }) => {
+ await profile.set(Profiles.SuperAdmin);
+ const kb = new KnowbaseItemPage(page);
+
+ const id = await api.createItem('KnowbaseItem', {
+ name: 'KB entry for filetype error test',
+ entities_id: getWorkerEntityId(),
+ answer: "Article content",
+ });
+
+ await kb.goto(id);
+
+ // Open the document upload modal
+ await page.getByRole('button', { name: 'Add Document' }).click();
+ const modal = page.getByRole('dialog');
+ await expect(modal).toBeVisible();
+
+ // Select a file with disallowed type (.json)
+ const filePaths = [path.join(__dirname, '../../fixtures/uploads/test.json')];
+ const fileChooserPromise = page.waitForEvent('filechooser');
+ await modal.getByText('Drop files here or click to browse').click();
+ const fileChooser = await fileChooserPromise;
+ await fileChooser.setFiles(filePaths);
+
+ // Wait for the file to appear in preview
+ await expect(modal.getByRole('listitem')).toHaveCount(1);
+
+ // Verify error is shown on the file item
+ await expect(modal.getByRole('listitem').first()).toContainText('Filetype not allowed');
+
+ // Verify upload button stays disabled
+ await expect(modal.getByRole('button', { name: 'Upload Documents' })).toBeDisabled();
+});
+
+test('Upload button is disabled without files', async ({ page, profile, api }) => {
+ await profile.set(Profiles.SuperAdmin);
+ const kb = new KnowbaseItemPage(page);
+
+ const id = await api.createItem('KnowbaseItem', {
+ name: 'KB entry for button state test',
+ entities_id: getWorkerEntityId(),
+ answer: "Article content",
+ });
+
+ await kb.goto(id);
+
+ // Open the document upload modal
+ await page.getByRole('button', { name: 'Add Document' }).click();
+ const modal = page.getByRole('dialog');
+ await expect(modal).toBeVisible();
+
+ // Verify the upload button is disabled
+ const uploadButton = modal.getByRole('button', { name: 'Upload Documents' });
+ await expect(uploadButton).toBeDisabled();
+
+ // Select a file
+ await kb.doSelectFilesForKbUpload(['uploads/foo.png'], modal);
+
+ // Verify the upload button is now enabled
+ await expect(uploadButton).toBeEnabled();
+});