diff --git a/InfoLogger/public/app.css b/InfoLogger/public/app.css index b3decfe96..712a6c467 100644 --- a/InfoLogger/public/app.css +++ b/InfoLogger/public/app.css @@ -264,3 +264,14 @@ a.disabled { pointer-events: none; cursor: default; } flex-direction: column; gap: 0.25rem; } + +.checkbox-container { + display: inline-flex; + gap: 6px; +} + +.status-number { + font-variant-numeric: tabular-nums; + display: inline-block; + text-align: right; +} diff --git a/InfoLogger/public/log/Log.js b/InfoLogger/public/log/Log.js index 03608b52c..14db2010a 100644 --- a/InfoLogger/public/log/Log.js +++ b/InfoLogger/public/log/Log.js @@ -529,12 +529,20 @@ export default class Log extends Observable { /** * Method to check if current selected mode is live and is running - * @returns {boolean} is live mode running + * @returns {boolean} - is live mode running */ isLiveModeRunning() { return this.activeMode === MODE.LIVE.RUNNING; } + /** + * Method to check if current selected mode is query + * @returns {boolean} - is query mode + */ + isQueryMode() { + return this.activeMode === MODE.QUERY; + } + /** * Set log's table UI sizes to allow log scrolling * @param {number} scrollTop - position of the user's scroll cursor @@ -556,6 +564,9 @@ export default class Log extends Observable { this.model.inspectorEnabled = false; this.resetStats(); this.queryResult = RemoteData.notAsked(); + if (this.isLiveModeRunning()) { + this.setAutoScrollLive(true, false); + } this.notify(); } @@ -575,14 +586,30 @@ export default class Log extends Observable { } /** - * Enable or disable auto-scroll for live mode, a checkbox is used to control it + * Focus the log table */ - toggleAutoScroll() { - this.autoScrollLive = !this.autoScrollLive; + focusTable() { this.dom.table.focus(); + this.dom.table.scrollTo(0, this.dom.table.scrollHeight); this.notify(); } + /** + * 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 about the change + */ + setAutoScrollLive(enabled, notify = true) { + if (this.autoScrollLive === enabled) { + return; + } + this.autoScrollLive = enabled; + + if (notify) { + this.notify(); + } + } + /** * Returns whether the live mode service is available * @returns {boolean} true if the live mode service is available, false otherwise diff --git a/InfoLogger/public/log/commandLogs.js b/InfoLogger/public/log/commandLogs.js index 1b10e0d54..840ecec00 100644 --- a/InfoLogger/public/log/commandLogs.js +++ b/InfoLogger/public/log/commandLogs.js @@ -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, diff --git a/InfoLogger/public/log/statusBar.js b/InfoLogger/public/log/statusBar.js index 79e08f66f..adabfe2d5 100644 --- a/InfoLogger/public/log/statusBar.js +++ b/InfoLogger/public/log/statusBar.js @@ -12,21 +12,24 @@ * or submit itself to any jurisdiction. */ -import { h } from '/js/src/index.js'; +import { + h, + iconWarning, +} from '/js/src/index.js'; /** * Bottom bar, showing status of the log's list and its details, - * some application messages and some basic options like auto-scroll checkbox. + * some application messages and some basic options like auto-scroll button. * @param {Model} model - root model of the application * @returns {vnode} - the view of the bottom bar */ export default (model) => [ - h('.flex-row', { id: 'status-bar' }, [ - h('', { style: 'width:50%' }, statusLogs(model)), - h('', { style: 'text-align: center; width:30%' }, sqlQuery(model)), + h('.flex-row.items-center.mv2.ph2.gc3', { id: 'status-bar' }, [ + h('.flex-grow.flex-wrap.items-center.gc2', { style: 'width:50%' }, statusLogs(model)), + model.log.isQueryMode() && h('.flex-grow', { style: 'text-align: center; width:30%' }, sqlQuery(model)), h( - '.flex-grow.text-right', - { id: 'status-bar-application-options' }, + '.flex-row.flex-grow.items-center.gc2', + { id: 'status-bar-application-options', style: 'justify-content: flex-end;' }, applicationMessage(model), applicationOptions(model), ), @@ -90,23 +93,33 @@ const applicationMessage = (model) => model.log.list.length > model.log.applicat : null; /** - * Show some application preferences: auto-scroll and inspector checkboxes + * Show some application preferences: auto-scroll button and inspector checkbox * (could be evolve into a preference panel in the future if more options are added) * @param {Model} model - root model of the application * @returns {vnode} - the view of the application options */ const applicationOptions = (model) => [ - h('label.d-inline', { title: 'Scroll down in live mode on new log incoming' }, h('input', { - type: 'checkbox', - checked: model.log.autoScrollLive, - onchange: () => model.log.toggleAutoScroll(), - }), ' Autoscroll'), - h('span.mh1'), - h('label.d-inline', { title: 'Show details of selected log' }, h('input', { + model.log.isLiveModeRunning() + ? model.log.autoScrollLive + ? h('span.success', { id: 'status-bar-auto-scroll', title: 'Autoscroll is active' }, 'Autoscroll Active') + : h( + 'button.btn.btn-sm.btn-warning', + { + id: 'status-bar-auto-scroll', + title: 'Click here / Scroll down to the bottom of the screen to reactivate', + onclick: () => model.log.setAutoScrollLive(true), + }, + [ + iconWarning(), + h('span.d-inline', 'Autoscroll Inactive'), + ], + ) + : null, + h('label.checkbox-container.m0.items-center', { title: 'Show details of selected log' }, h('input', { type: 'checkbox', checked: model.inspectorEnabled, onchange: () => model.toggleInspector(), - }), ' Inspector'), + }), 'Inspector'), ]; /** @@ -131,29 +144,42 @@ const statusLive = (model, frameworkInfo) => * @param {Model} model - root model of the application * @returns {vnode} - the view of the log's list status */ -const statusStats = (model) => [ - h( - 'span.ph1', - { - id: 'status-bar-buffer-size', - }, - [ - bufferStatus(model), - `${model.log.list.length.toLocaleString('en-US')} / ${model.log.limit.toLocaleString('en-US')} (Buffer size)`, - ], - ), - model.log.queryResult.match({ - NotAsked: () => null, - Loading: () => 'Querying server...', - Success: (result) => statusQuery(model, result), - Failure: () => null, // notification - }), - h('span.ph1.severity-d', `${model.log.stats.debug} debug`), - h('span.ph1.severity-i', `${model.log.stats.info} info`), - h('span.ph1.severity-w', `${model.log.stats.warning} warn`), - h('span.ph1.severity-e', `${model.log.stats.error} error`), - h('span.ph1.severity-f', `${model.log.stats.fatal} fatal`), -]; +const statusStats = (model) => { + const limitText = model.log.limit.toLocaleString('en-US'); + return [ + h( + 'span', + { + id: 'status-bar-buffer-size', + }, + [ + bufferStatus(model), + statNumber(model.log.list.length), + ` / ${limitText} (Buffer size)`, + ], + ), + model.log.queryResult.match({ + NotAsked: () => null, + Loading: () => 'Querying server...', + Success: (result) => statusQuery(model, result), + Failure: () => null, // notification + }), + h('span.severity-d', [statNumber(model.log.stats.debug), ' debug']), + h('span.severity-i', [statNumber(model.log.stats.info), ' info']), + h('span.severity-w', [statNumber(model.log.stats.warning), ' warn']), + h('span.severity-e', [statNumber(model.log.stats.error), ' error']), + h('span.severity-f', [statNumber(model.log.stats.fatal), ' fatal']), + ]; +}; + +/** + * Number to display in the status bar in a locale aware format + * Numbers are also made to be all the same width to avoid shifting in the status bar + * @param {number} value - number to display + * @returns {vnode} - the view of the number + */ +const statNumber = (value) => + h('span.status-number', value.toLocaleString('en-US')); const bufferStatus = (model) => { let dotClass = 'gray-darker'; // grey - unknown status default @@ -164,7 +190,8 @@ const bufferStatus = (model) => { dotClass = 'success'; // green - limit not reached } - return h(`span.${dotClass}.f7.mh1`, { + return h(`span.${dotClass}.f7`, { + style: 'margin-right: var(--space-xs)', title: model.log.limitReached === null ? 'No query data loaded' : model.log.limitReached === true ? 'Limit reached - results may be incomplete' : 'Limit OK', id: 'status-bar-buffer-dot', diff --git a/InfoLogger/public/log/tableLogsContent.js b/InfoLogger/public/log/tableLogsContent.js index 2250078ca..b55b39f1c 100644 --- a/InfoLogger/public/log/tableLogsContent.js +++ b/InfoLogger/public/log/tableLogsContent.js @@ -203,30 +203,125 @@ const tableContainerHooks = (model) => ({ * @param {vnode} vnode - the vnode of the element */ oncreate(vnode) { + const container = vnode.dom; + let isPointerDown = false; + let lastScrollTop = Math.max(container.scrollTop, 0); + /** - * THis handler allow to notify model of element scrolling change (.tableLogsContent) + * Update the viewport size and scroll position in the model */ - const onTableScroll = () => { - const container = vnode.dom; + const updateViewport = () => { 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; + /** + * Whether the viewport is at the bottom of the table, <=1 tolerates fractional heights (zoom / HiDPI) + * @returns {boolean} true if at the bottom + */ + const isAtBottom = () => container.scrollHeight - Math.max(container.scrollTop, 0) - container.clientHeight <= 1; + + /** + * Disable auto-scroll when the user scrolls towards older logs + */ + const onUserScrollUp = () => { + if (model.log.autoScrollLive && container.scrollTop > 0) { + model.log.setAutoScrollLive(false); + } + }; + + /** + * Resume auto-scroll when the user scrolls to the bottom in live mode. + * Called from the input itself rather than waiting for the scroll to land, as a smooth scroll + * animation can fall short if logs arrive in the meantime. + */ + const resumeAutoScrollIfLive = () => { + if (model.log.isLiveModeRunning()) { + model.log.setAutoScrollLive(true); + } + }; + + /** + * Mouse wheel and touchpad scrolling + * @param {WheelEvent} e - wheel event + */ + const onWheel = (e) => { + // ctrl/cmd + wheel is zoom, handled by Model + if (e.ctrlKey || e.metaKey) { + return; + } + if (e.deltaY < 0) { + onUserScrollUp(); + } + const maxScrollTop = container.scrollHeight - container.clientHeight; + if (e.deltaY > 0 && container.scrollTop + e.deltaY >= maxScrollTop - 1) { + resumeAutoScrollIfLive(); + } + }; + + /** + * Keyboard scrolling, arrow keys are handled by Model to move the selected log + * @param {KeyboardEvent} e - keyboard event + */ + const onKeyDown = (e) => { + const isTyping = e.target.closest('input, textarea, select, [contenteditable="true"]'); + const isOtherTarget = e.target !== document.body && !container.contains(e.target); + if (isTyping || isOtherTarget) { + return; + } + + if (e.key === 'PageUp' || e.key === 'Home' || e.key === ' ' && e.shiftKey) { + onUserScrollUp(); + } else if (e.key === 'End') { + resumeAutoScrollIfLive(); + } + }; + + const setPointerDown = (down) => () => { + isPointerDown = down; + }; + + /** + * Re-enable auto-scroll when a downward scroll reaches the bottom, then update the viewport. + * Disabling is done by the user input handlers above, except pointer drags which do both. + * Only a downward move counts, so a shrinking table or the start of a scroll up does not re-enable it. + */ + const onTableScroll = () => { + const scrollTop = Math.max(container.scrollTop, 0); + const isMovingDown = scrollTop > lastScrollTop; + lastScrollTop = scrollTop; + + // programmatic scroll is set by the live auto-scroll and when jumping to a selected log + if (container.isProgrammaticScroll) { + container.isProgrammaticScroll = false; + } else if (model.log.isLiveModeRunning()) { + if (isPointerDown) { + model.log.setAutoScrollLive(isAtBottom()); + } else if (isMovingDown && isAtBottom()) { + model.log.setAutoScrollLive(true); + } + } + updateViewport(); + }; + + const listeners = new AbortController(); + const { signal } = listeners; + + container.addEventListener('scroll', onTableScroll, { signal }); + container.addEventListener('wheel', onWheel, { passive: true, signal }); + container.addEventListener('pointerdown', setPointerDown(true), { signal }); + window.addEventListener('pointerup', setPointerDown(false), { signal }); + window.addEventListener('pointercancel', setPointerDown(false), { signal }); + window.addEventListener('keydown', onKeyDown, { signal }); + model.log.dom.table = container; // setup window size listener - view needs redraw for smart scrolling - window.addEventListener('resize', onTableScroll); + window.addEventListener('resize', updateViewport, { signal }); - // remember this function for later (destroy) - vnode.dom.onTableScroll = onTableScroll; + container.listeners = listeners; // call the function once on next frame when we know sizes - onTableScroll(); + updateViewport(); }, /** @@ -242,8 +337,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(); }, }); @@ -262,7 +356,11 @@ const autoscrollManager = (model, vnode) => { if (previousLastLogId !== currentLastLogId) { // scroll at maximum bottom possible + const previousScrollTop = vnode.dom.scrollTop; vnode.dom.scrollTo(0, model.log.rowHeight * model.log.applicationLimit); + if (vnode.dom.scrollTop !== previousScrollTop) { + vnode.dom.isProgrammaticScroll = true; + } vnode.dom.dataset.lastLogId = currentLastLogId; } @@ -270,6 +368,9 @@ const autoscrollManager = (model, vnode) => { return; } + // forget the last scrolled-to log so re-enabling auto-scroll jumps to the bottom straight away + delete vnode.dom.dataset.lastLogId; + // Autoscroll to selected item if (model.log.item) { // Scroll only if we did not previously, save last try in DOM dataset @@ -281,7 +382,12 @@ const autoscrollManager = (model, vnode) => { const index = model.log.list.indexOf(model.log.item); const positionRow = model.log.rowHeight * index; const halfView = model.log.scrollHeight / 2; + const previousScrollTop = vnode.dom.scrollTop; vnode.dom.scrollTo(0, positionRow - halfView); + // scrollTo updates scrollTop synchronously; the event fires later and only if the position moved + if (vnode.dom.scrollTop !== previousScrollTop) { + vnode.dom.isProgrammaticScroll = true; + } } // Save the fact that we changed `item` diff --git a/InfoLogger/public/view.js b/InfoLogger/public/view.js index 862f75c12..089980cad 100644 --- a/InfoLogger/public/view.js +++ b/InfoLogger/public/view.js @@ -75,7 +75,7 @@ export default (model) => [ logsTable(model), inspectorSide(model), ]), - h('footer.f7.ph1', [statusBar(model)]), + h('footer.f7', [statusBar(model)]), ], ), ]; diff --git a/InfoLogger/test/mocha-index.js b/InfoLogger/test/mocha-index.js index b8e50c737..2ee746510 100644 --- a/InfoLogger/test/mocha-index.js +++ b/InfoLogger/test/mocha-index.js @@ -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(); diff --git a/InfoLogger/test/public/live-mode-mocha.js b/InfoLogger/test/public/live-mode-mocha.js index 271786f8e..7664dce16 100644 --- a/InfoLogger/test/public/live-mode-mocha.js +++ b/InfoLogger/test/public/live-mode-mocha.js @@ -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); diff --git a/InfoLogger/test/public/log-context-menu-mocha.js b/InfoLogger/test/public/log-context-menu-mocha.js index ed2ba06e6..d194b37d3 100644 --- a/InfoLogger/test/public/log-context-menu-mocha.js +++ b/InfoLogger/test/public/log-context-menu-mocha.js @@ -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(() => { diff --git a/InfoLogger/test/public/logs-table-mocha.js b/InfoLogger/test/public/logs-table-mocha.js new file mode 100644 index 000000000..ed4052613 --- /dev/null +++ b/InfoLogger/test/public/logs-table-mocha.js @@ -0,0 +1,197 @@ +/** + * @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'); + +/** + * 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 }); +}; + +/** + * Fills the logs table with 200 logs and scrolls to the bottom to test autoscroll behavior. + * @param {Page} page - puppeteer page + */ +const fillTableAndScrollToBottom = async (page) => { + // ensure table has more rows than fit on the screen + 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); +}; + +/** + * 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, as autoscroll reacts to user input rather than scroll position + * @param {Page} page - puppeteer page + * @param {number} deltaY - wheel delta, negative scrolls up + */ +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} 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')); + await page.evaluate(() => model.zoom.resetZoom()); + await page.evaluate(() => model.log.empty()); + await page.waitForFunction(() => model.log.dom.table.scrollTop === 0); + }); + + 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 page.click('#live-button'); + await assertAutoScrollLive(page, true); + + // wait until live logs overflow the table and autoscroll has moved it down + await waitForScrollPastBottom(page); + + await wheelOverTable(page, -100); + await waitForAnimationFrame(page, 2); + + await assertAutoScrollLive(page, false); + }); + + it('should re-enable autoscroll when the user scrolls back to the bottom', async () => { + await page.click('#live-button'); + await waitForScrollPastBottom(page); + + await wheelOverTable(page, -100); + await waitForAnimationFrame(page, 2); + await assertAutoScrollLive(page, false); + + await wheelOverTable(page, 100000); + await waitForAnimationFrame(page, 2); + await page.waitForFunction(() => model.log.autoScrollLive === true, { timeout: 5000 }); + }); + + it('should not disable autoscroll when switching from a full query table to live mode', async () => { + await fillTableAndScrollToBottom(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 page.click('#live-button'); + await assertAutoScrollLive(page, true); + + // wait until live logs overflow the table and autoscroll has moved it down + await waitForScrollPastBottom(page); + + await page.click('#clear-button'); + await waitForAnimationFrame(page, 2); + + await assertAutoScrollLive(page, true); + }); + + it('should not disable autoscroll when zooming in', async () => { + await page.click('#live-button'); + await assertAutoScrollLive(page, true); + await waitForScrollPastBottom(page); + + await page.evaluate(() => new Promise((resolve) => { + // queue a scroll event as the autoscroll jump does (1px still counts as the bottom)... + model.log.dom.table.scrollTop -= 1; + // ...and zoom before that event is handled, as with a fast zoom click + model.zoom.zoomIn(); + requestAnimationFrame(() => requestAnimationFrame(resolve)); + })); + + await assertAutoScrollLive(page, true); + }); + + it('should re-enable autoscroll when the log list is cleared', async () => { + await page.click('#live-button'); + await waitForScrollPastBottom(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 change autoscroll when Home is pressed in a filter input', async () => { + await page.click('#live-button'); + await waitForScrollPastBottom(page); + + await page.focus('.text-area-for-message'); + await page.keyboard.press('Home'); + await waitForAnimationFrame(page, 2); + + await assertAutoScrollLive(page, true); + }); + }); + }); +}); diff --git a/InfoLogger/test/public/status-bar-mocha.js b/InfoLogger/test/public/status-bar-mocha.js index 3c0d46609..4ae1219a3 100644 --- a/InfoLogger/test/public/status-bar-mocha.js +++ b/InfoLogger/test/public/status-bar-mocha.js @@ -26,7 +26,7 @@ async function getSeverityCounts(page) { const counts = {}; const severityLabels = ['d', 'i', 'w', 'e', 'f']; severityLabels.forEach((label) => { - const el = document.querySelector(`span.ph1.severity-${label}`); + const el = document.querySelector(`.severity-${label} > .status-number`); const count = Number(el.textContent.trim().split(' ')[0]); counts[label.toUpperCase()] = count; }); @@ -35,7 +35,7 @@ async function getSeverityCounts(page) { } describe('Status Bar test-suite', async () => { - const AUTOSCROLL_SELECTOR = '#status-bar-application-options label[title*="Scroll down"] input'; + const AUTOSCROLL_SELECTOR = '#status-bar-auto-scroll'; const INSPECTOR_SELECTOR = '#status-bar-application-options label[title*="Show details"] input'; const SQL_QUERY = 'SELECT * FROM `messages` WHERE `timestamp`>=? AND `severity` IN (?) ' + 'ORDER BY `TIMESTAMP` LIMIT 100000'; @@ -47,6 +47,10 @@ describe('Status Bar test-suite', async () => { await page.goto(test.helpers.baseUrl, { waitUntil: 'networkidle0' }); }); + after(async () => { + await page.evaluate(() => model.log.liveStop('Query')); + }); + it('should be visible on the page', async () => { const statusBar = await page.$('#status-bar'); assert.ok(statusBar, 'status bar not found'); @@ -319,14 +323,57 @@ describe('Status Bar test-suite', async () => { }); describe('Application Options', () => { - it('should toggle autoscroll when its checkbox is clicked', async () => { - await page.click(AUTOSCROLL_SELECTOR); + describe('Autoscroll Option', () => { + beforeEach(async () => { + await page.evaluate(() => model.log.liveStop('Query')); + await page.waitForSelector('#live-button:not([disabled])'); + }); - const checkedAfter = await page.$eval(AUTOSCROLL_SELECTOR, (el) => el.checked); - const autoScrollModelValue = await page.evaluate(() => window.model.log.autoScrollLive); + it('should not display autoscroll option when in query mode', async () => { + const autoscrollOption = await page.$(AUTOSCROLL_SELECTOR); + assert.strictEqual(autoscrollOption, null); + }); - assert.strictEqual(autoScrollModelValue, checkedAfter); - assert.strictEqual(checkedAfter, true); + it('should display autoscroll option when in live running mode', async () => { + await page.click('#live-button'); + + const autoscrollOption = await page.waitForSelector(AUTOSCROLL_SELECTOR); + assert.notStrictEqual(autoscrollOption, null); + }); + + it('should not display autoscroll option when in live paused mode', async () => { + await page.click('#live-button'); + await page.waitForSelector(AUTOSCROLL_SELECTOR); + await page.click('#live-button'); + + await page.waitForFunction( + (selector) => !document.querySelector(selector), + { timeout: 2000 }, + AUTOSCROLL_SELECTOR, + ); + }); + + it('should show autoscroll as active when enabled in live mode', async () => { + await page.click('#live-button'); + await waitForTextInElement(page, AUTOSCROLL_SELECTOR, 'Autoscroll Active'); + }); + + it('should show autoscroll as inactive when disabled in live mode', async () => { + await page.click('#live-button'); + await page.evaluate(() => model.log.setAutoScrollLive(false)); + await waitForTextInElement(page, AUTOSCROLL_SELECTOR, 'Autoscroll Inactive'); + }); + + it('should re-enable autoscroll when inactive button is clicked', async () => { + await page.click('#live-button'); + await page.evaluate(() => model.log.setAutoScrollLive(false)); + await waitForTextInElement(page, AUTOSCROLL_SELECTOR, 'Autoscroll Inactive'); + + await page.click(AUTOSCROLL_SELECTOR); + + await waitForTextInElement(page, AUTOSCROLL_SELECTOR, 'Autoscroll Active'); + assert.strictEqual(await page.evaluate(() => model.log.autoScrollLive), true); + }); }); it('should toggle inspector when its checkbox is clicked', async () => {