From e14cb7a728e3ad3d9a92b7aad627ebb1d30ebf91 Mon Sep 17 00:00:00 2001 From: Rom1-B <8530352+Rom1-B@users.noreply.github.com> Date: Fri, 7 Aug 2026 16:07:51 +0200 Subject: [PATCH 1/2] Feat: upgrade dhtmlx-gantt to v10 --- .github/workflows/e2e.yml | 137 ++++++++++++++++++++++++++++ CHANGELOG.md | 10 ++ package-lock.json | 10 +- package.json | 2 +- public/css/gantt.scss | 10 +- public/js/gantt-helper.js | 45 ++++++--- public/js/libs.js | 4 +- templates/view.html.twig | 4 +- tests/e2e/fixtures/gantt_fixture.ts | 30 ++++++ tests/e2e/pages/GanttPage.ts | 104 +++++++++++++++++++++ tests/e2e/specs/gantt.spec.ts | 115 +++++++++++++++++++++++ tests/js/gantt-libs.test.js | 6 +- tsconfig.json | 11 +++ 13 files changed, 457 insertions(+), 31 deletions(-) create mode 100644 .github/workflows/e2e.yml create mode 100644 tests/e2e/fixtures/gantt_fixture.ts create mode 100644 tests/e2e/pages/GanttPage.ts create mode 100644 tests/e2e/specs/gantt.spec.ts create mode 100644 tsconfig.json diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 0000000..89bb2a9 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,137 @@ +# Standalone Playwright e2e job, not part of the shared +# glpi-project/plugin-ci-workflows reusable CI (see its README for why: this +# needs a dedicated e2e_testing database/environment and browser install, +# which the shared workflow does not provide). +# +# To reuse this file in another plugin: copy it as-is and update PLUGIN_KEY +# below, plus the specs path if it differs from tests/e2e/specs. +name: "End-to-end tests" + +on: + push: + branches: + - "main" + tags: + - "*" + pull_request: + workflow_dispatch: + +concurrency: + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +env: + PLUGIN_KEY: "gantt" + +jobs: + e2e: + name: "Playwright" + runs-on: "ubuntu-latest" + container: + image: "ghcr.io/glpi-project/githubactions-glpi-apache:php-8.2-glpi-11.0.x" + # `--user github-actions-runner` is mandatory to prevent rights issues on mounted volume during checkout operation + options: >- + --volume ${{ github.workspace }}:/var/www/glpi/plugins:rw + --user github-actions-runner + services: + db: + image: "ghcr.io/glpi-project/githubactions-mariadb:10.6" + env: + MYSQL_ALLOW_EMPTY_PASSWORD: "yes" + MYSQL_DATABASE: "glpi_e2e" + options: >- + --shm-size=1g + defaults: + run: + # By default, execute commands using the `www-data` user to prevent rights issues on GLPI generated files. + shell: "sudo --set-home --user=www-data --preserve-env bash --noprofile --norc -eo pipefail {0}" + working-directory: "/var/www/glpi" + steps: + - name: "Checkout" + uses: "actions/checkout@v7" + with: + path: "${{ env.PLUGIN_KEY }}" + + - name: "Fix plugin directory ownership" + # Use default `bash` shell with `github-actions-runner` user, the only + # one allowed passwordless sudo. + shell: "bash" + run: | + sudo setfacl --recursive --modify u:www-data:rwx "/var/www/glpi/plugins/${{ env.PLUGIN_KEY }}" + + - name: "Mark plugin directory as safe for git" + run: | + git config --global --add safe.directory "/var/www/glpi/plugins/${{ env.PLUGIN_KEY }}" + + - name: "Restore composer cache" + uses: "actions/cache@v6" + with: + path: "/var/www/glpi/plugins/${{ env.PLUGIN_KEY }}/vendor" + key: "${{ env.PLUGIN_KEY }}-composer-${{ hashFiles(format('{0}/composer.lock', env.PLUGIN_KEY)) }}" + + - name: "Install composer dependencies" + working-directory: "/var/www/glpi/plugins/${{ env.PLUGIN_KEY }}" + run: | + composer install --ansi --no-interaction --no-progress --prefer-dist + + - name: "Install e2e database" + # Port 8090 is the vhost bound to GLPI_ENVIRONMENT_TYPE=e2e_testing in + # this image; port 80 serves a different (unconfigured) environment. + run: | + bin/console database:install --ansi --no-interaction --force --reconfigure --no-telemetry \ + --db-host=db --db-name=glpi_e2e --db-user=root --db-password="" \ + --env=e2e_testing + bin/console config:set url_base "http://localhost:8090" --env=e2e_testing + bin/console config:set url_base_api "http://localhost:8090/apirest.php" --env=e2e_testing + + - name: "Install and activate plugin" + run: | + bin/console plugin:install --ansi --no-interaction --username=glpi "${{ env.PLUGIN_KEY }}" --env=e2e_testing + bin/console plugin:activate --ansi --no-interaction "${{ env.PLUGIN_KEY }}" --env=e2e_testing + + - name: "Run apache" + # Use default `bash` shell with `github-actions-runner` user + shell: "bash" + run: | + sudo service apache2 start + + - name: "Read installed Playwright version" + id: "playwright-version" + # Use default `bash` shell with `github-actions-runner` user: only this + # user can write to the runner-owned $GITHUB_OUTPUT file. + shell: "bash" + run: | + echo "version=$(node -p "require('@playwright/test/package.json').version")" >> "$GITHUB_OUTPUT" + + - name: "Restore Playwright browser cache" + id: "playwright-cache" + uses: "actions/cache@v6" + with: + path: "/home/www-data/.cache/ms-playwright" + key: "playwright-chromium-${{ steps.playwright-version.outputs.version }}" + + - name: "Install Playwright browser" + if: "steps.playwright-cache.outputs.cache-hit != 'true'" + run: | + npx playwright install chromium + + - name: "Install Playwright system dependencies" + # Use default `bash` shell with `github-actions-runner` user: this + # command needs passwordless sudo to install system packages as root. + shell: "bash" + run: | + npx playwright install-deps chromium + + - name: "Playwright" + env: + E2E_BASE_URL: "http://localhost:8090" + run: | + npx playwright test --project="plugin:${{ env.PLUGIN_KEY }}" --reporter=html + + - name: "Upload Playwright report" + if: ${{ !cancelled() }} + uses: "actions/upload-artifact@v7" + with: + name: "playwright-report" + path: "/var/www/glpi/tests/e2e/results" + retention-days: 7 diff --git a/CHANGELOG.md b/CHANGELOG.md index ba03579..414eb18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [Unreleased] + +### Changed + +- Upgraded dhtmlx-gantt to v10. + +### Added + +- Added Playwright e2e tests covering the Gantt view (render, today highlight, drag and drop with rollback). + ## [1.3.4] - 2026-08-04 ### Added diff --git a/package-lock.json b/package-lock.json index 20a6cad..aeaa08c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "hasInstallScript": true, "license": "GPL-2.0-or-later", "dependencies": { - "dhtmlx-gantt": "^9.1.4" + "dhtmlx-gantt": "^10.0.1" }, "devDependencies": { "clean-webpack-plugin": "^4.0.0", @@ -644,10 +644,10 @@ } }, "node_modules/dhtmlx-gantt": { - "version": "9.1.4", - "resolved": "https://registry.npmjs.org/dhtmlx-gantt/-/dhtmlx-gantt-9.1.4.tgz", - "integrity": "sha512-XCNA5QUiuV79Xq1ykNpH9LFNR2IVpDZMqnmBV6dsBeOkHyPMOpkyQ/gqAPCcK2GAvYHoN2nGAMYb2LldCWhMuQ==", - "license": "GPL-2.0" + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/dhtmlx-gantt/-/dhtmlx-gantt-10.0.1.tgz", + "integrity": "sha512-dVmq5WONubm+kODQKpDdTwTMNe9N6LEVVgmgsn/umcv2kPKrSnld7P4gEuw61W5RcB5n8d46nl0GJar4CRM3LA==", + "license": "MIT" }, "node_modules/electron-to-chromium": { "version": "1.5.400", diff --git a/package.json b/package.json index f376e81..6662e63 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "license": "GPL-2.0-or-later", "dependencies": { - "dhtmlx-gantt": "^9.1.4" + "dhtmlx-gantt": "^10.0.1" }, "scripts": { "build": "webpack --config .webpack.config.js", diff --git a/public/css/gantt.scss b/public/css/gantt.scss index 87f269a..8424449 100644 --- a/public/css/gantt.scss +++ b/public/css/gantt.scss @@ -185,9 +185,9 @@ } } -.gantt_today, .gantt_marker.today { - color: var(--dhx-gantt-base-colors-white); - font-size: var(--dhx-gantt-caption-font-size); - line-height: var(--dhx-gantt-caption-line-height); - font-weight: var(--dhx-gantt-caption-line-weight); +// highlights the current day column in the timeline (replaces the dhtmlx-gantt marker plugin, dropped in v10 community edition) +.gantt_task_cell.today { + background-color: rgb(255, 0, 0, 8%); + border-left: 1px solid red; + border-right: 1px solid red; } diff --git a/public/js/gantt-helper.js b/public/js/gantt-helper.js index 4a4096f..9b41fa1 100644 --- a/public/js/gantt-helper.js +++ b/public/js/gantt-helper.js @@ -38,6 +38,7 @@ const GlpiGantt = (function() { const url = `${plugin_path }/ajax/gantt.php`; const parseDateFormat = "%Y-%m-%d %H:%i"; let uiDateFormat = null; + let lastDragState = null; switch (CFG_GLPI.date_format) { case 1: uiDateFormat = '%d-%m-%Y'; @@ -166,20 +167,21 @@ const GlpiGantt = (function() { // enable tooltips and fullscreen mode gantt.plugins({ tooltip: true, - fullscreen: true, - undo: true, - marker: true + fullscreen: true }); - gantt.config.show_marker = true; gantt.config.current_date = new Date(); - const today = new Date(); - gantt.addMarker({ - start_date: today, - css: "today", - text: __("Today", 'gantt') - }); + // highlight the current day column (no marker plugin in dhtmlx-gantt 10 community edition) + gantt.templates.timeline_cell_class = (task, date) => { + const today = new Date(); + if (date.getFullYear() === today.getFullYear() + && date.getMonth() === today.getMonth() + && date.getDate() === today.getDate()) { + return "today"; + } + return ""; + }; gantt.templates.tooltip_text = (start, end, task) => { let text = `${ @@ -324,7 +326,20 @@ const GlpiGantt = (function() { }); if (!readonly) { - // catch task drag event to update db + // snapshot task state before a drag, to allow rolling back on server error + // (dhtmlx-gantt 10 community edition dropped the undo plugin) + gantt.attachEvent("onBeforeTaskDrag", (id) => { + const task = gantt.getTask(id); + lastDragState = { + id, + start_date: task.start_date, + end_date: task.end_date, + progress: task.progress + }; + return true; + }); + + // catch task drag event to update db gantt.attachEvent("onAfterTaskDrag", (id) => { const task = gantt.getTask(id); const progress = (Math.round(task.progress * 100 / 5) * 5) / 100; // prevent server side exception for wrong stepping @@ -765,7 +780,13 @@ const GlpiGantt = (function() { displayAjaxMessageAfterRedirect(); } else { gantt.alert(__('Could not update Task[%s]: ', 'gantt').replace('%s', task.text) + json.error); - gantt.undo(); + if (lastDragState && lastDragState.id === task.id) { + task.start_date = lastDragState.start_date; + task.end_date = lastDragState.end_date; + task.progress = lastDragState.progress; + gantt.updateTask(task.id); + gantt.render(); + } } } }); diff --git a/public/js/libs.js b/public/js/libs.js index 80c3375..09b1f7f 100644 --- a/public/js/libs.js +++ b/public/js/libs.js @@ -26,6 +26,8 @@ * ------------------------------------------------------------------------- */ -import gantt from 'dhtmlx-gantt/codebase/dhtmlxgantt.js'; +// v10's UMD bundle (dhtmlxgantt.js) isn't statically analyzable by webpack, +// so its default export resolves to undefined; the real ESM build works. +import gantt from 'dhtmlx-gantt/codebase/dhtmlxgantt.es.js'; window.gantt = gantt; import 'dhtmlx-gantt/codebase/dhtmlxgantt.css'; diff --git a/templates/view.html.twig b/templates/view.html.twig index 7f48618..816b6a2 100644 --- a/templates/view.html.twig +++ b/templates/view.html.twig @@ -27,7 +27,7 @@ #} -
+
  • @@ -57,7 +57,7 @@ - + diff --git a/tests/e2e/fixtures/gantt_fixture.ts b/tests/e2e/fixtures/gantt_fixture.ts new file mode 100644 index 0000000..60a0cd4 --- /dev/null +++ b/tests/e2e/fixtures/gantt_fixture.ts @@ -0,0 +1,30 @@ +/** + * ------------------------------------------------------------------------- + * gantt plugin for GLPI + * ------------------------------------------------------------------------- + * + * LICENSE + * + * This file is part of gantt. + * + * gantt 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 2 of the License, or + * any later version. + * + * gantt 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 gantt. If not, see . + * ------------------------------------------------------------------------- + * @copyright Copyright (C) 2013-2023 by gantt plugin team. + * @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html + * @link https://github.com/pluginsGLPI/gantt + * ------------------------------------------------------------------------- + */ + +// Re-export GLPI's own worker/profile/api fixtures, no plugin-specific fixture needed yet. +export * from '../../../../../tests/e2e/fixtures/glpi_fixture'; diff --git a/tests/e2e/pages/GanttPage.ts b/tests/e2e/pages/GanttPage.ts new file mode 100644 index 0000000..1212210 --- /dev/null +++ b/tests/e2e/pages/GanttPage.ts @@ -0,0 +1,104 @@ +/** + * ------------------------------------------------------------------------- + * gantt plugin for GLPI + * ------------------------------------------------------------------------- + * + * LICENSE + * + * This file is part of gantt. + * + * gantt 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 2 of the License, or + * any later version. + * + * gantt 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 gantt. If not, see . + * ------------------------------------------------------------------------- + * @copyright Copyright (C) 2013-2023 by gantt plugin team. + * @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html + * @link https://github.com/pluginsGLPI/gantt + * ------------------------------------------------------------------------- + */ + +import { Locator, Page } from '@playwright/test'; +import { GlpiPage } from '../../../../../tests/e2e/pages/GlpiPage'; + +/** Page helpers for the dhtmlx-gantt view rendered on a project's Gantt tab. */ +export class GanttPage extends GlpiPage { + public readonly container: Locator; + public readonly today_cells: Locator; + + public constructor(page: Page) { + super(page); + this.container = this.page.getByTestId('gantt-container'); + // eslint-disable-next-line playwright/no-raw-locators -- dhtmlx-gantt cell, no semantic hook available + this.today_cells = this.page.locator('.gantt_task_cell.today'); + } + + /** Navigates to a project's Gantt tab and waits for the chart to be ready. */ + public async goto(project_id: number): Promise { + await this.page.goto(`/front/project.form.php?id=${project_id}&forcetab=GlpiPlugin%5CGantt%5CProjectTab$1`); + await this.container.waitFor({ state: 'visible' }); + // The loader overlay is appended after the fetch starts, so wait for a row instead. + // eslint-disable-next-line playwright/no-raw-locators -- dhtmlx-gantt row, no semantic hook available + await this.page.locator('.gantt_row').first().waitFor({ state: 'visible' }); + } + + /** The rendered row for a task/project whose label contains the given text. */ + public getRow(label: string): Locator { + // eslint-disable-next-line playwright/no-raw-locators -- dhtmlx-gantt row, no semantic hook available + return this.page.locator('.gantt_row').filter({ hasText: label }).first(); + } + + /** Switches to the "Days" zoom level, the only one where each column is a single day. */ + public async zoomToDays(): Promise { + await this.page.getByTestId('gantt-zoom-days').click(); + // Zooming keeps the previous scroll offset, so scroll to today explicitly. + await this.page.evaluate(() => (window as any).gantt.showDate(new Date())); + } + + /** Reads the internal dhtmlx task id of the first task/project matching the given label. */ + public async getTaskId(label: string): Promise { + return this.page.evaluate((text) => { + let id: number | string | null = null; + (window as any).gantt.eachTask((task: any) => { + if (id === null && task.text === text) { + id = task.id; + } + }); + return id; + }, label); + } + + /** Reads the current parent id (dhtmlx data model) of the given task id. */ + public async getParentId(task_id: number | string): Promise { + return this.page.evaluate((id) => (window as any).gantt.getTask(id).parent, task_id); + } + + /** Drags a row onto another one to trigger dhtmlx's row reparenting. */ + public async dragRowOnto(source_label: string, target_label: string): Promise { + const source_box = await this.getRow(source_label).boundingBox(); + const target_box = await this.getRow(target_label).boundingBox(); + if (source_box === null || target_box === null) { + throw new Error('Could not locate the source or target row to drag'); + } + + // eslint-disable-next-line playwright/no-raw-locators -- dhtmlx-gantt internal drop marker, no semantic hook available + const drop_marker = this.page.locator('.gantt_grid_dnd_marker'); + + await this.page.mouse.move(source_box.x + 20, source_box.y + source_box.height / 2); + await this.page.mouse.down(); + await this.page.mouse.move(source_box.x + 20, source_box.y + source_box.height / 2 - 10, { steps: 5 }); + await drop_marker.waitFor({ state: 'visible' }); + await this.page.mouse.move(target_box.x + 20, target_box.y + target_box.height / 2, { steps: 10 }); + await drop_marker.waitFor({ state: 'visible' }); + await this.page.mouse.up(); + await drop_marker.waitFor({ state: 'hidden' }); + } +} diff --git a/tests/e2e/specs/gantt.spec.ts b/tests/e2e/specs/gantt.spec.ts new file mode 100644 index 0000000..1537af9 --- /dev/null +++ b/tests/e2e/specs/gantt.spec.ts @@ -0,0 +1,115 @@ +/** + * ------------------------------------------------------------------------- + * gantt plugin for GLPI + * ------------------------------------------------------------------------- + * + * LICENSE + * + * This file is part of gantt. + * + * gantt 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 2 of the License, or + * any later version. + * + * gantt 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 gantt. If not, see . + * ------------------------------------------------------------------------- + * @copyright Copyright (C) 2013-2023 by gantt plugin team. + * @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html + * @link https://github.com/pluginsGLPI/gantt + * ------------------------------------------------------------------------- + */ + +import { expect, test } from '../fixtures/gantt_fixture'; +import { GanttPage } from '../pages/GanttPage'; +import { Profiles } from '../../../../../tests/e2e/utils/Profiles'; +import { getWorkerEntityId } from '../../../../../tests/e2e/utils/WorkerEntities'; + +// Marks a changeParent() request among gantt.php's other POST actions. +const CHANGE_ITEM_PARENT_FLAG = 'changeItemParent=1'; + +function toGlpiDate(date: Date): string { + return date.toISOString().slice(0, 19).replace('T', ' '); +} + +test.describe('Gantt view', () => { + // Wide enough (past and future) that "today" always falls inside the range. + const today = new Date(); + const plan_start = new Date(today); + plan_start.setDate(plan_start.getDate() - 5); + const plan_end = new Date(today); + plan_end.setDate(plan_end.getDate() + 10); + + test('renders, highlights today, and reparents a task via drag and drop', async ({ page, profile, api }) => { + await profile.set(Profiles.SuperAdmin); + + const project_id = await api.createItem('Project', { + name: `E2E Gantt project ${test.info().workerIndex}-${Date.now()}`, + entities_id: getWorkerEntityId(), + plan_start_date: toGlpiDate(plan_start), + plan_end_date: toGlpiDate(plan_end), + }); + + await api.createItem('ProjectTask', { + name: 'E2E Task A', + projects_id: project_id, + entities_id: getWorkerEntityId(), + plan_start_date: toGlpiDate(plan_start), + plan_end_date: toGlpiDate(today), + }); + await api.createItem('ProjectTask', { + name: 'E2E Task B', + projects_id: project_id, + entities_id: getWorkerEntityId(), + plan_start_date: toGlpiDate(today), + plan_end_date: toGlpiDate(plan_end), + }); + await api.createItem('ProjectTask', { + name: 'E2E Task C', + projects_id: project_id, + entities_id: getWorkerEntityId(), + plan_start_date: toGlpiDate(today), + plan_end_date: toGlpiDate(plan_end), + }); + + const gantt = new GanttPage(page); + await gantt.goto(project_id); + + // Renders: the tasks created via the API are all visible in the timeline. + await expect(gantt.getRow('E2E Task A')).toBeVisible(); + await expect(gantt.getRow('E2E Task B')).toBeVisible(); + await expect(gantt.getRow('E2E Task C')).toBeVisible(); + + // Today highlight: replaces the marker plugin dropped in v10 community edition. + await gantt.zoomToDays(); + await expect(gantt.today_cells.first()).toBeVisible(); + + // Drag and drop: reparenting Task B under Task A persists across a reload. + const task_a_id_client = await gantt.getTaskId('E2E Task A'); + await gantt.dragRowOnto('E2E Task B', 'E2E Task A'); + const task_b_id = await gantt.getTaskId('E2E Task B'); + expect(await gantt.getParentId(task_b_id!)).toBe(task_a_id_client); + + await gantt.goto(project_id); + expect(await gantt.getParentId(task_b_id!)).toBe(task_a_id_client); + + // Rollback only happens on an explicit { ok: false } response, not on a transport error. + await page.route('**/ajax/gantt.php', async (route) => { + const post_data = route.request().postData() ?? ''; + if (post_data.includes(CHANGE_ITEM_PARENT_FLAG)) { + await route.fulfill({ json: { ok: false, error: 'Simulated rejection' } }); + return; + } + await route.continue(); + }); + await gantt.dragRowOnto('E2E Task C', 'E2E Task A'); + const task_c_id = await gantt.getTaskId('E2E Task C'); + expect(String(await gantt.getParentId(task_c_id!))).toBe(String(project_id)); + }); +}); diff --git a/tests/js/gantt-libs.test.js b/tests/js/gantt-libs.test.js index c5ebedf..0e00734 100644 --- a/tests/js/gantt-libs.test.js +++ b/tests/js/gantt-libs.test.js @@ -39,18 +39,14 @@ describe('dhtmlx-gantt bundle (public/lib/libs.js)', () => { expect(typeof window.gantt).toBe('object'); }); - test('enables the tooltip/fullscreen/undo/marker plugins used by gantt-helper.js', () => { + test('enables the tooltip/fullscreen plugins used by gantt-helper.js', () => { expect(() => { window.gantt.plugins({ tooltip: true, fullscreen: true, - undo: true, - marker: true, }); }).not.toThrow(); - expect(typeof window.gantt.addMarker).toBe('function'); - expect(typeof window.gantt.undo).toBe('function'); expect(typeof window.gantt.ext.fullscreen).toBe('object'); expect(typeof window.gantt.ext.zoom).toBe('object'); }); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..293c8bb --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "skipLibCheck": true, + "strict": true, + "target": "ES2022", + "module": "commonjs", + "moduleResolution": "node", + "esModuleInterop": true + }, + "include": ["tests/e2e/**/*.ts"] +} From 43ddc67211de46e1fb76bbe3f81558488502f26c Mon Sep 17 00:00:00 2001 From: Rom1-B <8530352+Rom1-B@users.noreply.github.com> Date: Mon, 10 Aug 2026 07:41:14 +0200 Subject: [PATCH 2/2] fix CI --- .github/workflows/e2e.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 89bb2a9..8e90daa 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -74,6 +74,19 @@ jobs: run: | composer install --ansi --no-interaction --no-progress --prefer-dist + - name: "Restore npm cache" + uses: "actions/cache@v6" + with: + path: "/var/www/glpi/plugins/${{ env.PLUGIN_KEY }}/node_modules" + key: "${{ env.PLUGIN_KEY }}-npm-${{ hashFiles(format('{0}/package-lock.json', env.PLUGIN_KEY)) }}" + + - name: "Build plugin frontend assets" + # `npm install` triggers the `postinstall` script, which runs the webpack + # build; its output (public/lib, public/build) is gitignored. + working-directory: "/var/www/glpi/plugins/${{ env.PLUGIN_KEY }}" + run: | + npm install + - name: "Install e2e database" # Port 8090 is the vhost bound to GLPI_ENVIRONMENT_TYPE=e2e_testing in # this image; port 80 serves a different (unconfigured) environment.