fix: add listener to repaint on visibility change for canvas (#28568)

This commit is contained in:
Elizabeth Thompson 2024-05-21 09:52:32 -07:00 committed by GitHub
parent 0d5aec12d4
commit 62a0336425
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 222 additions and 0 deletions

View File

@ -250,6 +250,7 @@
"ignore-styles": "^5.0.1",
"imports-loader": "^3.1.1",
"jest": "^26.6.3",
"jest-canvas-mock": "^2.5.2",
"jest-environment-enzyme": "^7.1.2",
"jest-enzyme": "^7.1.2",
"jest-websocket-mock": "^2.2.0",
@ -31171,6 +31172,12 @@
"resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz",
"integrity": "sha1-xtJnJjKi5cg+AT5oZKQs6N79IK4="
},
"node_modules/cssfontparser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/cssfontparser/-/cssfontparser-1.2.1.tgz",
"integrity": "sha512-6tun4LoZnj7VN6YeegOVb67KBX/7JJsqvj+pv3ZA7F878/eN33AbGa5b/S/wXxS/tcp8nc40xRUrsPlxIyNUPg==",
"dev": true
},
"node_modules/cssnano": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/cssnano/-/cssnano-6.0.3.tgz",
@ -41175,6 +41182,16 @@
"node": ">= 10.14.2"
}
},
"node_modules/jest-canvas-mock": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/jest-canvas-mock/-/jest-canvas-mock-2.5.2.tgz",
"integrity": "sha512-vgnpPupjOL6+L5oJXzxTxFrlGEIbHdZqFU+LFNdtLxZ3lRDCl17FlTMM7IatoRQkrcyOTMlDinjUguqmQ6bR2A==",
"dev": true,
"dependencies": {
"cssfontparser": "^1.2.1",
"moo-color": "^1.0.2"
}
},
"node_modules/jest-changed-files": {
"version": "26.6.2",
"resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-26.6.2.tgz",
@ -52539,6 +52556,21 @@
"resolved": "https://registry.npmjs.org/moo/-/moo-0.4.3.tgz",
"integrity": "sha512-gFD2xGCl8YFgGHsqJ9NKRVdwlioeW3mI1iqfLNYQOv0+6JRwG58Zk9DIGQgyIaffSYaO1xsKnMaYzzNr1KyIAw=="
},
"node_modules/moo-color": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/moo-color/-/moo-color-1.0.3.tgz",
"integrity": "sha512-i/+ZKXMDf6aqYtBhuOcej71YSlbjT3wCO/4H1j8rPvxDJEifdwgg5MaFyu6iYAT8GBZJg2z0dkgK4YMzvURALQ==",
"dev": true,
"dependencies": {
"color-name": "^1.1.4"
}
},
"node_modules/moo-color/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"node_modules/mousetrap": {
"version": "1.6.5",
"resolved": "https://registry.npmjs.org/mousetrap/-/mousetrap-1.6.5.tgz",
@ -96422,6 +96454,12 @@
"resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz",
"integrity": "sha1-xtJnJjKi5cg+AT5oZKQs6N79IK4="
},
"cssfontparser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/cssfontparser/-/cssfontparser-1.2.1.tgz",
"integrity": "sha512-6tun4LoZnj7VN6YeegOVb67KBX/7JJsqvj+pv3ZA7F878/eN33AbGa5b/S/wXxS/tcp8nc40xRUrsPlxIyNUPg==",
"dev": true
},
"cssnano": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/cssnano/-/cssnano-6.0.3.tgz",
@ -104272,6 +104310,16 @@
}
}
},
"jest-canvas-mock": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/jest-canvas-mock/-/jest-canvas-mock-2.5.2.tgz",
"integrity": "sha512-vgnpPupjOL6+L5oJXzxTxFrlGEIbHdZqFU+LFNdtLxZ3lRDCl17FlTMM7IatoRQkrcyOTMlDinjUguqmQ6bR2A==",
"dev": true,
"requires": {
"cssfontparser": "^1.2.1",
"moo-color": "^1.0.2"
}
},
"jest-changed-files": {
"version": "26.6.2",
"resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-26.6.2.tgz",
@ -112794,6 +112842,23 @@
"resolved": "https://registry.npmjs.org/moo/-/moo-0.4.3.tgz",
"integrity": "sha512-gFD2xGCl8YFgGHsqJ9NKRVdwlioeW3mI1iqfLNYQOv0+6JRwG58Zk9DIGQgyIaffSYaO1xsKnMaYzzNr1KyIAw=="
},
"moo-color": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/moo-color/-/moo-color-1.0.3.tgz",
"integrity": "sha512-i/+ZKXMDf6aqYtBhuOcej71YSlbjT3wCO/4H1j8rPvxDJEifdwgg5MaFyu6iYAT8GBZJg2z0dkgK4YMzvURALQ==",
"dev": true,
"requires": {
"color-name": "^1.1.4"
},
"dependencies": {
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
}
}
},
"mousetrap": {
"version": "1.6.5",
"resolved": "https://registry.npmjs.org/mousetrap/-/mousetrap-1.6.5.tgz",

View File

@ -316,6 +316,7 @@
"ignore-styles": "^5.0.1",
"imports-loader": "^3.1.1",
"jest": "^26.6.3",
"jest-canvas-mock": "^2.5.2",
"jest-environment-enzyme": "^7.1.2",
"jest-enzyme": "^7.1.2",
"jest-websocket-mock": "^2.2.0",

View File

@ -90,6 +90,8 @@ class Dashboard extends React.PureComponent {
this.appliedFilters = props.activeFilters ?? {};
this.appliedOwnDataCharts = props.ownDataCharts ?? {};
this.onVisibilityChange = this.onVisibilityChange.bind(this);
this.handleVisibilityChange = this.handleVisibilityChange.bind(this);
this.repaintCanvas = this.repaintCanvas.bind(this);
}
componentDidMount() {
@ -192,6 +194,24 @@ class Dashboard extends React.PureComponent {
this.props.actions.clearDataMaskState();
}
repaintCanvas(canvas, ctx, imageBitmap) {
// Clear the canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw the copied content
ctx.drawImage(imageBitmap, 0, 0);
}
handleVisibilityChange() {
this.canvases.forEach(canvas => {
const ctx = canvas.getContext('2d');
createImageBitmap(canvas).then(imageBitmap => {
// Call the repaintCanvas function with canvas, ctx, and imageBitmap
this.repaintCanvas(canvas, ctx, imageBitmap);
});
});
}
onVisibilityChange() {
if (document.visibilityState === 'hidden') {
// from visible to hidden
@ -199,6 +219,7 @@ class Dashboard extends React.PureComponent {
start_offset: Logger.getTimestamp(),
ts: new Date().getTime(),
};
this.canvases = document.querySelectorAll('canvas');
} else if (document.visibilityState === 'visible') {
// from hidden to visible
const logStart = this.visibilityEventData.start_offset;
@ -206,6 +227,8 @@ class Dashboard extends React.PureComponent {
...this.visibilityEventData,
duration: Logger.getTimestamp() - logStart,
});
// for chrome to ensure that the canvas doesn't disappear
this.handleVisibilityChange();
}
}

View File

@ -19,6 +19,7 @@
import React from 'react';
import { shallow } from 'enzyme';
import sinon from 'sinon';
import 'jest-canvas-mock';
import Dashboard from 'src/dashboard/components/Dashboard';
import { CHART_TYPE } from 'src/dashboard/util/componentTypes';
@ -38,6 +39,7 @@ import { dashboardLayout } from 'spec/fixtures/mockDashboardLayout';
import dashboardState from 'spec/fixtures/mockDashboardState';
import { sliceEntitiesForChart as sliceEntities } from 'spec/fixtures/mockSliceEntities';
import { getAllActiveFilters } from 'src/dashboard/util/activeAllDashboardFilters';
import { Logger, LOG_ACTIONS_HIDE_BROWSER_TAB } from '../../logger/LogUtils';
describe('Dashboard', () => {
const props = {
@ -245,5 +247,136 @@ describe('Dashboard', () => {
expect(refreshSpy.callCount).toBe(1);
expect(refreshSpy.getCall(0).args[0]).toEqual([]);
});
// The canvas is cleared using the clearRect method.
it('should clear the canvas using clearRect method', () => {
// Arrange
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const imageBitmap = new ImageBitmap(100, 100);
// Act
wrapper.instance().repaintCanvas(canvas, ctx, imageBitmap);
// Assert
expect(ctx.clearRect).toHaveBeenCalledWith(
0,
0,
canvas.width,
canvas.height,
);
});
// The canvas width and height are 0.
it('should recreate the canvas with the same dimensions', () => {
// Arrange
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const imageBitmap = new ImageBitmap(100, 100);
// Act
wrapper.instance().repaintCanvas(canvas, ctx, imageBitmap);
// Assert
const { width, height } = ctx.canvas;
expect(canvas.width).toBe(width);
expect(canvas.height).toBe(height);
});
// When the document visibility state changes to 'hidden', the method sets the 'visibilityEventData' object with a 'start_offset' and 'ts' properties, and queries all canvas elements on the page and stores them in the 'canvases' property.
it('should set visibilityEventData and canvases when document visibility state changes to "hidden"', () => {
// Initialize the class object with props
const props = {
activeFilters: {},
ownDataCharts: {},
actions: {
logEvent: jest.fn(),
},
layout: {},
dashboardInfo: {},
dashboardState: {
editMode: false,
isPublished: false,
hasUnsavedChanges: false,
},
chartConfiguration: {},
};
const DATE_TO_USE = new Date('2020');
const OrigDate = Date;
global.Date = jest.fn(() => DATE_TO_USE);
global.Date.UTC = OrigDate.UTC;
global.Date.parse = OrigDate.parse;
global.Date.now = OrigDate.now;
// Your test code here
const dashboard = new Dashboard(props);
// Mock the return value of document.visibilityState
jest.spyOn(document, 'visibilityState', 'get').mockReturnValue('hidden');
// mock Logger.getTimestamp() to return a fixed value
jest.spyOn(Logger, 'getTimestamp').mockReturnValue(1234567890);
// Invoke the method
dashboard.onVisibilityChange();
// Assert that visibilityEventData is set correctly
expect(dashboard.visibilityEventData).toEqual({
start_offset: 1234567890,
ts: DATE_TO_USE.getTime(),
});
// Assert that canvases are queried correctly
expect(dashboard.canvases).toEqual(expect.any(NodeList));
// Restore the original implementation of document.visibilityState
jest.restoreAllMocks();
// After your test
global.Date = OrigDate;
});
// When the document visibility state changes to 'visible', the method logs an event and calls the 'handleVisibilityChange' method.
it('should log an event and call handleVisibilityChange when document visibility state changes to "visible"', () => {
// Initialize the class object
const dashboard = new Dashboard({ activeFilters: {} });
// Mock the props and actions
dashboard.props = {
actions: {
logEvent: jest.fn(),
},
};
// Mock the visibilityEventData
dashboard.visibilityEventData = {
start_offset: 123,
ts: 456,
};
// Mock the handleVisibilityChange method
dashboard.handleVisibilityChange = jest.fn();
// Mock the document.visibilityState property
jest.spyOn(document, 'visibilityState', 'get').mockReturnValue('visible');
// Invoke the method
dashboard.onVisibilityChange();
// Assert that logEvent is called with the correct arguments
expect(dashboard.props.actions.logEvent).toHaveBeenCalledWith(
LOG_ACTIONS_HIDE_BROWSER_TAB,
{
...dashboard.visibilityEventData,
duration: expect.any(Number),
},
);
// Assert that handleVisibilityChange is called
expect(dashboard.handleVisibilityChange).toHaveBeenCalled();
// Restore the original implementation of document.visibilityState
jest.restoreAllMocks();
});
});
});