Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
f4e511e
Revert "Bump symfony/serializer from 7.3.3 to 7.3.4 (#333)"
erseco Oct 6, 2025
290a474
Merge branch 'main' of github.com:exelearning/exelearning
erseco Oct 6, 2025
3af8f5b
Merge branch 'main' of github.com:exelearning/exelearning
erseco Oct 7, 2025
acc625a
Refactored e2e test framework to use factories
erseco Oct 8, 2025
cc38e3b
Merge branch 'main' of github.com:exelearning/exelearning
erseco Oct 8, 2025
01ba0a5
Merge branch 'main' of github.com:exelearning/exelearning into featur…
erseco Oct 8, 2025
b6c174e
Makefile cleanup
erseco Oct 8, 2025
840f9c6
Test to down the container
erseco Oct 8, 2025
37c500b
Fix /structureEngine.js 841:37 Uncaught TypeError: Cannot read proper…
erseco Oct 8, 2025
93ff61a
Test origin
erseco Oct 8, 2025
7a9bb34
increase timen
erseco Oct 8, 2025
80f7f87
Fix cors
erseco Oct 8, 2025
b8495da
Fix cors
erseco Oct 8, 2025
646653b
Fix deprecations
erseco Oct 8, 2025
30cb593
Added iDevice initial E2E tests
erseco Oct 8, 2025
ce25cac
Added Node RealTime inital test
erseco Oct 8, 2025
98f0012
removed down of pipeline
erseco Oct 8, 2025
6db24b7
Merge branch 'main' of github.com:exelearning/exelearning
erseco Oct 9, 2025
ac4a70d
Merge branch 'main' of github.com:exelearning/exelearning
erseco Oct 9, 2025
b39c448
Merge branch 'main' of github.com:exelearning/exelearning into featur…
erseco Oct 9, 2025
d1e4553
Merge branch 'main' of github.com:exelearning/exelearning
erseco Oct 9, 2025
68d4e63
Fix prama sqlite settings for tests
erseco Oct 9, 2025
69b2fee
Merge branch 'main' of github.com:exelearning/exelearning into featur…
erseco Oct 9, 2025
babfba4
Enable test in pipeline again
erseco Oct 9, 2025
c7a48c4
Merge branch 'hotfix/fix-pragma-on-test-and-deprecation' of github.co…
erseco Oct 9, 2025
3605e83
Remove foreign keys PRAGMA setting for SQLite
erseco Oct 9, 2025
e58d997
Remove foreign keys assertion and concurrent writers test
erseco Oct 9, 2025
348363b
Merge branch 'hotfix/fix-pragma-on-test-and-deprecation' of github.co…
erseco Oct 9, 2025
75b8ee5
Simlify offline tests
erseco Oct 9, 2025
6e921bf
Added data-testid and fixes tests
erseco Oct 9, 2025
de87f3a
Fix OpenElpTest
erseco Oct 9, 2025
eaa8df9
Try again
erseco Oct 9, 2025
224aa0a
Try again
erseco Oct 9, 2025
b2d4200
Defensive deletion of nodes
erseco Oct 9, 2025
c9b59c1
Fixed timeouts
erseco Oct 9, 2025
8e8d201
Fixed timeouts
erseco Oct 9, 2025
c632b1a
Code cleanup
erseco Oct 9, 2025
2116361
Code cleanup
erseco Oct 9, 2025
072c49b
Code cleanup
erseco Oct 9, 2025
80cf148
Increse test time
erseco Oct 9, 2025
6d0a226
Increse test time
erseco Oct 9, 2025
7f31862
Fix js errors
erseco Oct 9, 2025
828c9af
Mark realtime test incomplete
erseco Oct 9, 2025
d011db5
Increse test time
erseco Oct 9, 2025
410d8ce
Increse test time
erseco Oct 9, 2025
265f720
Increse test time
erseco Oct 9, 2025
0178644
Easier tests
erseco Oct 9, 2025
25a94b3
Fix 500 error and bs.tooltip error
erseco Oct 10, 2025
310fb0b
Fix 500 error and bs.tooltip error
erseco Oct 10, 2025
578f198
Fix anoter 500 error
erseco Oct 10, 2025
d33c78f
Added data-testid to html templates to easy locate selectores in testing
erseco Oct 10, 2025
9f35df8
Merge branch 'main' of github.com:exelearning/exelearning into featur…
erseco Oct 10, 2025
27d60ca
Merge branch 'main' of github.com:exelearning/exelearning into featur…
erseco Oct 10, 2025
9b19685
Merge branch 'main' of github.com:exelearning/exelearning into featur…
erseco Oct 10, 2025
0ff2a00
Skipped two teste because #head-top-download-button is not yet available
erseco Oct 10, 2025
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
13 changes: 6 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,14 @@ jobs:
- name: PHPUnit Unit Tests
run: make test-unit

# TODO: Temporary disabled e2e tests
# - name: PHPUnit E2E Tests
# run: make test-e2e
- name: PHPUnit E2E Tests
run: make test-e2e

# - name: PHPUnit E2E RealTime Tests
# run: make test-e2e-realtime
- name: PHPUnit E2E RealTime Tests
run: make test-e2e-realtime

# - name: PHPUnit E2E Offline Tests
# run: make test-e2e-offline
- name: PHPUnit E2E Offline Tests
run: make test-e2e-offline

- name: Generate HTML report
if: always()
Expand Down
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ RUN apk add --no-cache \
COPY --chown=nobody assets.conf /etc/nginx/server-conf.d/assets.conf
COPY --chown=nobody idevices.conf /etc/nginx/server-conf.d/idevices.conf
COPY --chown=nobody subdir.conf.template /etc/nginx/server-conf.d/subdir.conf.template
COPY --chown=nobody nginx-logging-map.conf /etc/nginx/conf.d/logging-map.conf

# Copy Mercure binary and configuration from the official container because Mercure is not yet available as an Alpine package
COPY --from=dunglas/mercure:latest /usr/bin/caddy /usr/bin/mercure
Expand Down Expand Up @@ -119,4 +120,4 @@ COPY --chown=nobody . .
RUN rm /app/02-configure-symfony.sh

HEALTHCHECK --interval=1m --timeout=15s --start-period=1m --retries=3 \
CMD curl -f http://localhost:8080/healthcheck || exit 1
CMD curl --fail --silent --show-error http://localhost:8080/healthcheck || exit 1
33 changes: 28 additions & 5 deletions main.js
Original file line number Diff line number Diff line change
Expand Up @@ -1018,11 +1018,34 @@ function startPhpServer() {
});

phpServer.stderr.on('data', (data) => {
const errorMessage = data.toString();
console.error(`PHP Error: ${errorMessage}`);
if (errorMessage.includes('Address already in use')) {
showErrorDialog(`Port ${customEnv.APP_PORT} is already in use. Close the process using it and try again.`);
app.quit();
// Normalize to string
const text = data instanceof Buffer ? data.toString() : String(data);

// Process line by line (chunks can arrive concatenated)
for (const raw of text.split(/\r?\n/)) {
const line = raw.trim();
if (!line) continue;

// Silence php -S noise: "[::1]:64324 Accepted" / "[::1]:64324 Closing"
if (/\[(?:::1|127\.0\.0\.1)\]:\d+\s+(?:Accepted|Closing)\s*$/i.test(line)) {
continue;
}

// Hide simple succesful access logs like [200] or [301]
// Example: "[::1]:64331 [200]: GET /path" | "[::1]:64335 [301]: POST /path"
if (/\[\s*(?:200|301)\s*\]:\s+(GET|POST|PUT|DELETE|HEAD|OPTIONS)\s+/i.test(line)) {
continue;
}

// Detect "Address already in use" and stop the app
if (line.includes('Address already in use')) {
showErrorDialog(`Port ${customEnv.APP_PORT} is already in use. Close the process using it and try again.`);
app.quit();
return;
}

// Keep useful stderr
console.warn(`${line}`);
}
});

Expand Down
14 changes: 14 additions & 0 deletions mercure.conf
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,20 @@ location ^~ /.well-known/mercure {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";

# Always expose the CORS headers required by the E2E browser tests
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Credentials;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Allow-Methods;
add_header Access-Control-Allow-Origin $http_origin always;
add_header Access-Control-Allow-Credentials "true" always;
add_header Access-Control-Allow-Headers "Authorization,Content-Type" always;
add_header Access-Control-Allow-Methods "GET,POST,OPTIONS" always;

if ($request_method = 'OPTIONS') {
return 204;
}

# Return 400 if the proxy fails
error_page 502 503 504 =400 /mercure_400.html;
}
Expand Down
18 changes: 14 additions & 4 deletions mercure.run
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,25 @@
# Pipe stderr to stdout
exec 2>&1

if [ "$APP_ONLINE_MODE" -eq 0 ]; then
# If online mode is explicitly disabled, do not start Mercure
if [ "${APP_ONLINE_MODE:-1}" = "0" ]; then
echo "Mercure is disabled: APP_ONLINE_MODE=0"
exec sleep infinity
fi

# Check APP_ENV and run mercure with the appropriate configuration
if [ "$APP_ENV" = "dev" ]; then
case "${APP_ENV:-}" in
dev)
# In dev we will have the mercure UI at http://<url>/.well-known/mercure/ui/
exec /usr/bin/mercure run --config /etc/caddy/dev.Caddyfile --adapter caddyfile
else
;;
test)
# In test, run Mercure quietly: redirect logs away from stdout to avoid noisy test output.
mkdir -p /mnt/data/mercure/log
# Note: logs are available for inspection at /mnt/data/mercure/log/mercure-test.log
exec /usr/bin/mercure run --config /etc/caddy/dev.Caddyfile --adapter caddyfile >> /mnt/data/mercure/log/mercure-test.log 2>&1
;;
*)
exec /usr/bin/mercure run --config /etc/caddy/Caddyfile --adapter caddyfile
fi
;;
esac
17 changes: 17 additions & 0 deletions nginx-logging-map.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Map request URI to a flag that enables access logging for all requests
# except for the container healthcheck endpoint.
# This file is included at the `http` level via /etc/nginx/conf.d/.

map $request_uri $exe_loggable {
default 1; # log by default
/healthcheck 0; # skip logs for healthcheck
}


# Override access logging behavior at the server level to use the conditional
# variable defined in nginx-logging-map.conf. This disables the inherited
# http-level access_log and re-enables it conditionally.

access_log off;
access_log /dev/stdout main_timed if=$exe_loggable;

6 changes: 5 additions & 1 deletion phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,14 @@
<!-- <env name="PANTHER_APP_BASE_URI" value="http://exelearning:8080"/> -->
<!-- <env name="PANTHER_NO_SANDBOX" value="1"/> -->
<!-- <env name="PANTHER_ENABLED" value="0"/> -->
<!-- We are using our own approach to save the screenshots, to also write preview window when visible -->
<!-- <env name="PANTHER_ERROR_SCREENSHOT_DIR" value="/tmp/e2e_screenshots/"/> -->

<!-- Mercure -->
<!-- Internal hub URL used by the backend to publish (inside exelearning container). -->
<env name="MERCURE_URL" value="http://exelearning:8080/.well-known/mercure"/>
<env name="MERCURE_PUBLIC_URL" value=""/>
<!-- Public hub URL used by the browser (Chrome container) to subscribe/publish. -->
<env name="MERCURE_PUBLIC_URL" value="http://exelearning:8080/.well-known/mercure"/>
<env name="MERCURE_JWT_SECRET_KEY" value="!ChangeThisMercureHubJWTSecretKey!"/>

<!-- REST Api -->
Expand Down
60 changes: 55 additions & 5 deletions public/app/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,33 @@ class App {
);
}
}

// Test-env override: when running E2E with Panther, the page origin
// is the internal PHP server (exelearning:908X), which doesn't host Mercure.
// Force the hub to the Nginx/Caddy endpoint in the exelearning container.
if (window.eXeLearning?.symfony?.environment === 'test') {
// Only override if not already explicitly set to a non-908X host
try {
const current = window.eXeLearning.mercure?.url || '';
const url = new URL(current || 'http://exelearning');
const isPantherPort = /^90\d{2}$/.test(
String(urlRequest.port || '')
);
const isCurrentOk = current.includes('exelearning:8080');
if (!isCurrentOk && isPantherPort) {
window.eXeLearning.mercure = {
...(window.eXeLearning.mercure || {}),
url: 'http://exelearning:8080/.well-known/mercure',
};
}
} catch (e) {
// Fallback: set directly
window.eXeLearning.mercure = {
...(window.eXeLearning.mercure || {}),
url: 'http://exelearning:8080/.well-known/mercure',
};
}
}
}

/**
Expand Down Expand Up @@ -461,12 +488,35 @@ class App {
/**
* Prevent unexpected close
*
* Install the `beforeunload` handler only after the first real user gesture.
* If we install it eagerly on page load, Chrome (especially in headless/E2E
* contexts) blocks the confirmation panel and logs a SEVERE console warning:
* "Blocked attempt to show a 'beforeunload' confirmation panel for a frame
* that never had a user gesture since its load."
* Deferring the installation avoids noisy warnings during automated navigations
* while preserving the safety prompt for real users after they interact.
*/
window.onbeforeunload = function (event) {
event.preventDefault();
// Kept for legacy.
event.returnValue = false;
};
let __exeBeforeUnloadInstalled = false;
function __exeInstallBeforeUnloadOnce() {
if (__exeBeforeUnloadInstalled) return;
__exeBeforeUnloadInstalled = true;

window.onbeforeunload = function (event) {
event.preventDefault();
// Modern browsers ignore custom text; a non-empty value is still
// required to trigger the confirmation dialog.
event.returnValue = '';
};
}

// Listen for the first trusted user interaction and install then.
['pointerdown', 'touchstart', 'keydown', 'input'].forEach((type) => {
window.addEventListener(type, __exeInstallBeforeUnloadOnce, {
once: true,
passive: true,
capture: true,
});
});

/**
* Catch ctrl+z action
Expand Down
30 changes: 26 additions & 4 deletions public/app/common/app_common.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,32 @@ export default class Common {
* @returns {string}
*/
initTooltips(elm) {
$(".exe-app-tooltip", elm).tooltip();
$('.exe-app-tooltip', elm).on('click mouseleave', function(){
try {
const scope = elm instanceof Element ? elm : document;
const elems = scope.querySelectorAll('.exe-app-tooltip');
elems.forEach((el) => {
// Idempotent initialization: only create if not already bound
const existing = window.bootstrap?.Tooltip?.getInstance
? window.bootstrap.Tooltip.getInstance(el)
: null;
if (!existing && window.bootstrap?.Tooltip?.getOrCreateInstance) {
window.bootstrap.Tooltip.getOrCreateInstance(el);
// Hide on click/mouseleave like previous jQuery behavior
el.addEventListener('click', () => {
try { window.bootstrap.Tooltip.getInstance(el)?.hide(); } catch (_) {}
}, { passive: true });
el.addEventListener('mouseleave', () => {
try { window.bootstrap.Tooltip.getInstance(el)?.hide(); } catch (_) {}
}, { passive: true });
}
});
} catch (_) {
// Fallback to jQuery plugin if Bootstrap global is not available
$(".exe-app-tooltip", elm).tooltip();
$('.exe-app-tooltip', elm).on('click mouseleave', function(){
$(this).tooltip('hide');
});
});
}
}

/**
Expand Down Expand Up @@ -82,4 +104,4 @@ export default class Common {
});
}

}
}
2 changes: 1 addition & 1 deletion public/app/workarea/interface/elements/concurrentUsers.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export default class ConcurrentUsers {
'#exe-concurrent-users'
);
this.currentUsersJson = null;
this.currentUsers = null;
this.currentUsers = []; // safer default
this.intervalTime = 3500;
this.app = app;
}
Expand Down
4 changes: 4 additions & 0 deletions public/app/workarea/interface/loadingScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export default class LoadingScreen {
show() {
this.loadingScreenNode.classList.remove('hide');
this.loadingScreenNode.classList.add('loading');
// Testing: explicit visibility flag
this.loadingScreenNode.setAttribute('data-visible', 'true');
}

/**
Expand All @@ -23,6 +25,8 @@ export default class LoadingScreen {
setTimeout(() => {
this.loadingScreenNode.classList.remove('hiding');
this.loadingScreenNode.classList.add('hide');
// Testing: explicit visibility flag
this.loadingScreenNode.setAttribute('data-visible', 'false');
}, this.hideTime);
}
}
5 changes: 5 additions & 0 deletions public/app/workarea/menus/idevices/menuIdevicesBottom.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ export default class MenuIdevicesBottom {
ideviceDiv.setAttribute('drag', 'idevice');
ideviceDiv.setAttribute('icon-type', ideviceData.icon.type);
ideviceDiv.setAttribute('icon-name', ideviceData.icon.name);
// Testing: quickbar item testid
ideviceDiv.setAttribute(
'data-testid',
`quick-idevice-${ideviceData.id}`
);
ideviceDiv.append(this.elementDivIcon(ideviceData));

return ideviceDiv;
Expand Down
4 changes: 4 additions & 0 deletions public/app/workarea/menus/idevices/menuIdevicesCompose.js
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,8 @@ export default class MenuIdevicesCompose {
ideviceDiv.setAttribute('drag', 'idevice');
ideviceDiv.setAttribute('icon-type', ideviceData.icon.type);
ideviceDiv.setAttribute('icon-name', ideviceData.icon.name);
// Testing: left menu id for this iDevice
ideviceDiv.setAttribute('data-testid', `idevice-${ideviceData.id}`);
ideviceDiv.append(this.elementDivIcon(ideviceData));
ideviceDiv.append(this.elementDivTitle(ideviceData.title));

Expand All @@ -471,6 +473,8 @@ export default class MenuIdevicesCompose {
ideviceDiv.setAttribute('title', ideviceData.title);
ideviceDiv.setAttribute('icon-type', ideviceData.icon.type);
ideviceDiv.setAttribute('icon-name', ideviceData.icon.name);
// Testing: left menu id for this imported iDevice
ideviceDiv.setAttribute('data-testid', `idevice-${ideviceData.id}`);

ideviceDiv.append(this.elementDivIcon(ideviceData));
ideviceDiv.append(this.elementDivTitle(ideviceData.title));
Expand Down
3 changes: 2 additions & 1 deletion public/app/workarea/menus/navbar/items/navbarUtilities.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ export default class NavbarFile {
*/
setTooltips() {
// See eXeLearning.app.common.initTooltips
$('.main-menu-right button')
// Avoid binding tooltips to dropdown toggles to prevent Bootstrap instance conflicts
$('.main-menu-right button:not([data-bs-toggle="dropdown"])')
.attr('data-bs-placement', 'bottom')
.tooltip();
$('#exeUserMenuToggler').on('click mouseleave', function () {
Expand Down
Loading
Loading