Fix code highlighting and optimize category select

This commit introduces a batched debounce mechanism for managing user
selection state changes. It effectively reduces unnecessary processing
during rapid script checking, preventing multiple triggers for code
compilation and UI rendering.

Key improvements include:

- Enhanced performance, especially noticeable when selecting large
  categories. This update resolves minor UI freezes experienced when
  selecting categories with numerous scripts.
- Correction of a bug where the code area only highlighted the last
  selected script when multiple scripts were chosen.

Other changes include:

- Timing functions:
  - Create a `Timing` folder for `throttle` and the new
    `batchedDebounce` functions.
  - Move these functions to the application layer from the presentation
    layer, reflecting their application-wide use.
  - Refactor existing code for improved clarity, naming consistency, and
    adherence to new naming conventions.
  - Add missing unit tests.
- `UserSelection`:
  - State modifications in `UserSelection` now utilize a singular object
    inspired by the CQRS pattern, enabling batch updates and flexible
    change configurations, thereby simplifying change management.
- Remove the `I` prefix from related interfaces to align with new coding
  standards.
- Refactor related code for better testability in isolation with
  dependency injection.
- Repository:
  - Move repository abstractions to the application layer.
  - Improve repository abstraction to combine `ReadonlyRepository` and
    `MutableRepository` interfaces.
- E2E testing:
  - Introduce E2E tests to validate the correct batch selection
    behavior.
  - Add a specialized data attribute in `TheCodeArea.vue` for improved
    testability.
  - Reorganize shared Cypress functions for a more idiomatic Cypress
    approach.
  - Improve test documentation with related information.
- `SelectedScript`:
  - Create an abstraction for simplified testability.
  - Introduce `SelectedScriptStub` in tests as a substitute for the
    actual object.
This commit is contained in:
undergroundwires
2023-11-18 22:23:27 +01:00
parent 4531645b4c
commit cb42f11b97
79 changed files with 2733 additions and 1351 deletions

View File

@@ -1,5 +1,5 @@
// eslint-disable-next-line max-classes-per-file
import { waitForHeaderBrandTitle } from './shared/ApplicationLoad';
import { getHeaderBrandTitle } from './support/interactions/header';
interface Stoppable {
stop(): void;
@@ -175,7 +175,7 @@ enum ApplicationLoadStep {
const checkpoints: Record<ApplicationLoadStep, () => void> = {
[ApplicationLoadStep.IndexHtmlLoaded]: () => cy.get('#app').should('be.visible'),
[ApplicationLoadStep.AppVueLoaded]: () => cy.get('.app__wrapper').should('be.visible'),
[ApplicationLoadStep.HeaderBrandTitleLoaded]: () => waitForHeaderBrandTitle(),
[ApplicationLoadStep.HeaderBrandTitleLoaded]: () => getHeaderBrandTitle(),
};
class ContinuousRunner implements Stoppable {

View File

@@ -0,0 +1,47 @@
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
import { openCard } from './support/interactions/card';
describe('script selection highlighting', () => {
it('highlights more when multiple scripts are selected', () => {
// Regression test for a bug where selecting multiple scripts only highlighted the last one.
cy.visit('/');
selectLastScript();
getCurrentHighlightRange((lastScriptHighlightRange) => {
cy.log(`Highlight height for last script: ${lastScriptHighlightRange}`);
cy.visit('/');
selectAllScripts();
getCurrentHighlightRange((allScriptsHighlightRange) => {
cy.log(`Highlight height for all scripts: ${allScriptsHighlightRange}`);
expect(allScriptsHighlightRange).to.be.greaterThan(lastScriptHighlightRange);
});
});
});
});
function selectLastScript() {
openCard({
cardIndex: -1, // last card
});
cy.get('.node')
.last()
.click({ force: true });
}
function selectAllScripts() {
cy.contains('span', 'All')
.click();
}
function getCurrentHighlightRange(
callback: (highlightedRange: number) => void,
) {
cy
.get('#codeEditor')
.invoke('attr', 'data-test-highlighted-range')
.should('not.be.empty')
.and('not.equal', '0')
.then((range) => {
expectExists(range);
callback(parseInt(range, 10));
});
}

View File

@@ -1,11 +1,11 @@
import { waitForHeaderBrandTitle } from './shared/ApplicationLoad';
import { getHeaderBrandTitle } from './support/interactions/header';
describe('application is initialized as expected', () => {
it('loads title as expected', () => {
// act
cy.visit('/');
// assert
waitForHeaderBrandTitle();
getHeaderBrandTitle();
});
it('there are no console.error output', () => {
// act

View File

@@ -1,12 +1,13 @@
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
import { openCard } from './support/interactions/card';
describe('revert toggle', () => {
context('toggle switch', () => {
beforeEach(() => {
cy.visit('/');
cy.get('.card')
.eq(1) // to get 2nd element, first is often cleanup that may lack revert button
.click(); // open the card card
openCard({
cardIndex: 1, // first is often cleanup that may lack revert button
});
cy.get('.toggle-switch')
.first()
.as('toggleSwitch');

View File

@@ -1,3 +0,0 @@
export function waitForHeaderBrandTitle() {
cy.contains('h1', 'privacy.sexy');
}

View File

@@ -0,0 +1,7 @@
export function openCard(options: {
readonly cardIndex: number;
}) {
cy.get('.card')
.eq(options.cardIndex)
.click();
}

View File

@@ -0,0 +1,3 @@
export function getHeaderBrandTitle() {
cy.contains('h1', 'privacy.sexy');
}