Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
25 changes: 19 additions & 6 deletions InfoLogger/public/log/Log.js
Original file line number Diff line number Diff line change
Expand Up @@ -547,15 +547,20 @@ export default class Log extends Observable {
}

/**
* Empty the list of all logs, reset stats, clear query mode request if any
* and close the inspector panel
* Empty the list of all logs, reset stats, clear query mode request if any,
* deselect the current log, close the inspector panel and re-enable auto-scroll in live mode
*/
empty() {
this.list = [];
this.item = null;
this.autoScrollToItem = false;
this.limitReached = null;
this.model.inspectorEnabled = false;
this.resetStats();
this.queryResult = RemoteData.notAsked();
if (this.isLiveModeRunning()) {
this.setAutoScrollLive(true, false);
}
this.notify();
}

Expand All @@ -575,11 +580,19 @@ export default class Log extends Observable {
}

/**
* Enable auto-scroll for live mode and scroll to the bottom of the table
* Set auto-scroll for live mode and optionally notify the UI
* @param {boolean} enabled - whether auto-scroll should be enabled
* @param {boolean} notify - whether to notify the UI if there is a change
*/
enableAutoScrollLiveAndNotify() {
this.autoScrollLive = true;
this.notify();
setAutoScrollLive(enabled, notify = true) {
if (this.autoScrollLive === enabled) {
return;
}
this.autoScrollLive = enabled;

if (notify) {
this.notify();
}
}

/**
Expand Down
2 changes: 1 addition & 1 deletion InfoLogger/public/log/commandLogs.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const LIVE_BUTTON_TYPES_BY_MODE = {
export const commandLogs = (model) => [
userActionsDropdown(model),
h('', interactionModesGroupButton(model)),
h('', h('button.btn', { onclick: () => model.log.empty(), style: 'font-weight: bold' }, 'Clear')),
h('', h('button.btn', { id: 'clear-button', onclick: () => model.log.empty(), style: 'font-weight: bold' }, 'Clear')),
h('.btn-group', [
h('button.btn', {
disabled: !model.log.list.length,
Expand Down
2 changes: 1 addition & 1 deletion InfoLogger/public/log/statusBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ const applicationOptions = (model) => [
{
id: 'status-bar-auto-scroll',
title: 'Click here to re-enable autoscroll',
onclick: () => model.log.enableAutoScrollLiveAndNotify(),
onclick: () => model.log.setAutoScrollLive(true),
},
[
iconWarning(),
Expand Down
72 changes: 53 additions & 19 deletions InfoLogger/public/log/tableLogsContent.js
Original file line number Diff line number Diff line change
Expand Up @@ -198,35 +198,70 @@ const linkToWikiErrors = (errcode) => h('a', {
const tableContainerHooks = (model) => ({

/**
* Hook. Listen to events needed for handling scrolling like window size change
* And set scroll change handler to internal state of dom element
* Hook. Listen to scroll and window size changes to keep the model's viewport in sync
* @param {vnode} vnode - the vnode of the element
*/
oncreate(vnode) {
const container = vnode.dom;

/**
* THis handler allow to notify model of element scrolling change (.tableLogsContent)
* Current scroll position, cancels negative position due to Safari bounce scrolling
* @returns {number} scrollTop clamped to 0
*/
const onTableScroll = () => {
const container = vnode.dom;
const getScrollTop = () => Math.max(container.scrollTop, 0);

/**
* Update the viewport size and scroll position in the model
* @param {number} scrollTop - current scroll position
*/
const updateViewport = (scrollTop = getScrollTop()) => {
const { height } = container.getBoundingClientRect();
const scrollTop = Math.max(container.scrollTop, 0); // cancel negative position due to Safari bounce scrolling
if (container.scrollTop < model.log.scrollTop) {
model.log.autoScrollLive = false; // stop auto-scrolling if user scrolls up
}
model.log.setScrollTop(scrollTop, height);
};

// call the function when scrolling is updated
vnode.dom.addEventListener('scroll', onTableScroll);
model.log.dom.table = vnode.dom;
// setup window size listener - view needs redraw for smart scrolling
window.addEventListener('resize', onTableScroll);
/**
* Whether the viewport is at the bottom of the table, <=1 tolerates fractional heights
* @param {number} scrollTop - current scroll position
* @returns {boolean} true if at the bottom
*/
const isAtBottom = (scrollTop) => container.scrollHeight - scrollTop - container.clientHeight <= 1;

/**
* Disable auto-scroll when the user scrolls up, then update the viewport.
* A shrinking table clamps scrollTop down but stays at the bottom, so it doesn't count.
* Needed as well as onTableWheel as onTableWheel doesn't cover all cases of user-initiated scrolls.
*/
const onTableScroll = () => {
const scrollTop = getScrollTop();
if (scrollTop < model.log.scrollTop && !isAtBottom(scrollTop)) {
model.log.setAutoScrollLive(false, false);
}
updateViewport(scrollTop);
};

// remember this function for later (destroy)
vnode.dom.onTableScroll = onTableScroll;
/**
* Disable auto-scroll as soon as the user wheels up.
* onTableScroll can miss them if an incoming log pushes the scroll to the bottom before the check runs.
* @param {WheelEvent} e - the wheel event
*/
const onTableWheel = (e) => {
if (e.deltaY < 0 && !e.ctrlKey && !e.metaKey && getScrollTop() > 0) {
model.log.setAutoScrollLive(false, false);
}
};

const listeners = new AbortController();
const { signal } = listeners;

container.addEventListener('scroll', onTableScroll, { signal });
container.addEventListener('wheel', onTableWheel, { signal, passive: true });
// setup window size listener - view needs redraw for smart scrolling
window.addEventListener('resize', () => updateViewport(), { signal });
container.listeners = listeners;
model.log.dom.table = container;

// call the function once on next frame when we know sizes
onTableScroll();
updateViewport();
},

/**
Expand All @@ -242,8 +277,7 @@ const tableContainerHooks = (model) => ({
* @param {vnode} vnode - the vnode of the element
*/
ondestroy(vnode) {
vnode.dom.removeEventListener('scroll', vnode.dom.onTableScroll);
window.removeEventListener('resize', vnode.dom.onTableScroll);
vnode.dom.listeners.abort();
},
});

Expand Down
1 change: 1 addition & 0 deletions InfoLogger/test/mocha-index.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ describe('InfoLogger', function () {
require('./public/status-bar-mocha');
require('./public/zoom.mocha');
require('./public/log-context-menu-mocha');
require('./public/logs-table-mocha');

after(async () => {
await browser.close();
Expand Down
4 changes: 4 additions & 0 deletions InfoLogger/test/public/live-mode-mocha.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ describe('Live Mode test-suite', async () => {
({ helpers: { baseUrl }, page } = test);
});

after(async () => {
await page.evaluate(() => model.log.liveStop('Query'));
});

it('should successfully go to homepage with predefined filters', async () => {
await page.goto(baseUrl, { waitUntil: 'networkidle0' });
const location = await page.evaluate(() => window.location);
Expand Down
4 changes: 4 additions & 0 deletions InfoLogger/test/public/log-context-menu-mocha.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ describe('Cell Context Menu', async () => {
});
});

after(async () => {
await page.evaluate(() => model.log.contextMenu.hide());
});

describe('Menu visibility', async () => {
it('should show context menu on right-click', async () => {
await page.evaluate(() => {
Expand Down
174 changes: 174 additions & 0 deletions InfoLogger/test/public/logs-table-mocha.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/**
* @license
* Copyright 2019-2020 CERN and copyright holders of ALICE O2.
* See http://alice-o2.web.cern.ch/copyright for details of the copyright holders.
* All rights not expressly granted are reserved.
*
* This software is distributed under the terms of the GNU General Public
* License v3 (GPL Version 3), copied verbatim in the file "COPYING".
*
* In applying this license CERN does not waive the privileges and immunities
* granted to it by virtue of its status as an Intergovernmental Organization
* or submit itself to any jurisdiction.
*/

const assert = require('assert');
const test = require('../mocha-index');
const { injectLogs } = require('../utils/utils');

/**
* Inserts and checks that the logs table is scrolled past the bottom.
* For Live mode inserting is much quicker than waiting for the table to naturally populate.
* @param {Page} page - puppeteer page
*/
const insertAndWaitForScrollPastBottom = async (page) => {
await injectLogs(page, Array.from({ length: 200 }, (_, i) => ({
severity: 'I',
message: `info log ${i}`,
timestamp: Date.now() + i,
})));
await waitForScrollPastBottom(page);
};

/**
* Waits for the logs table to be scrolled past the bottom.
* @param {Page} page - puppeteer page
*/
const waitForScrollPastBottom = async (page) => {
await page.waitForFunction(() => model.log.dom.table.scrollTop > 0, { timeout: 5000 });
};

/**
* Asserts that autoScrollLive is the expected value.
* @param {Page} page - puppeteer page
* @param {boolean} expected - expected value of autoScrollLive
*/
const assertAutoScrollLive = async (page, expected) => {
assert.strictEqual(await page.evaluate(() => model.log.autoScrollLive), expected);
};

/**
* Scrolls the logs table with a real mouse wheel event
* @param {Page} page - puppeteer page
* @param {number} deltaY - wheel delta
*/
const wheelOverTable = async (page, deltaY) => {
const box = await (await page.$('.tableLogsContent')).boundingBox();
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
await page.mouse.wheel({ deltaY });
};

/**
* Allows waiting for a specified number of animation frames
* Useful when dealing with scroll and layout changes that require dealing with what occurs in each animation frame.
* @param {Page} page - puppeteer page
* @param {*} frames - number of frames to wait for
* @returns {Promise<void>} resolves after the specified number of animation frames
*/
const waitForAnimationFrame = (page, frames = 1) =>
page.evaluate((frames) => new Promise((resolve) => {
const step = () => {
if (frames <= 0) {
resolve();
} else {
frames--;
requestAnimationFrame(step);
}
};
step();
}), frames);

describe('Logs Table test-suite', async () => {
let page = null;
let baseUrl = null;

before(async () => {
({ helpers: { baseUrl }, page } = test);
await page.goto(baseUrl, { waitUntil: 'networkidle0' });
});

describe('Autoscroll behavior', async () => {
describe('in live mode', async () => {
beforeEach(async () => {
await page.waitForSelector('#live-button:not([disabled])');
await page.evaluate(() => {
model.log.liveStop('Query');
model.zoom.resetZoom();
model.log.empty();
});
await page.waitForFunction(() => model.log.dom.table.scrollTop === 0);
await page.click('#live-button');
});

after(async () => {
await page.evaluate(() => model.log.liveStop('Query'));
await page.evaluate(() => model.zoom.resetZoom());
});

it('should disable autoscroll when the user scrolls up', async () => {
await assertAutoScrollLive(page, true);

await insertAndWaitForScrollPastBottom(page);

await wheelOverTable(page, -100);
await waitForAnimationFrame(page, 2);

await assertAutoScrollLive(page, false);
});

it('should not disable autoscroll when switching from a full query table to live mode', async () => {
await page.evaluate(() => model.log.liveStop('Query'));

await injectLogs(page, Array.from({ length: 200 }, (_, i) => ({
severity: 'I',
message: `info log ${i}`,
timestamp: Date.now() + i,
})));
await page.evaluate(() => model.log.goToLastItem());
await waitForScrollPastBottom(page);
await page.click('#live-button');

await waitForAnimationFrame(page, 2);

await assertAutoScrollLive(page, true);
});

it('should not disable autoscroll when the log list is cleared', async () => {
await assertAutoScrollLive(page, true);

await insertAndWaitForScrollPastBottom(page);

await page.click('#clear-button');
await waitForAnimationFrame(page, 2);

await assertAutoScrollLive(page, true);
});

it('should re-enable autoscroll when the log list is cleared', async () => {
await insertAndWaitForScrollPastBottom(page);

await wheelOverTable(page, -100);
await waitForAnimationFrame(page, 2);
await assertAutoScrollLive(page, false);

await page.click('#clear-button');
await waitForAnimationFrame(page, 2);

await assertAutoScrollLive(page, true);
});

it('should not disable autoscroll when zooming in and out', async () => {
await assertAutoScrollLive(page, true);
await insertAndWaitForScrollPastBottom(page);

await page.click('#zoom-in-button');
await waitForAnimationFrame(page, 2);
await assertAutoScrollLive(page, true);

await page.click('#zoom-out-button');
await waitForAnimationFrame(page, 2);
await assertAutoScrollLive(page, true);
});
});
});
});
Loading