Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add headless render tests using JSDOM. #629

Merged
merged 6 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
316 changes: 316 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"esbuild": "^0.24.0",
"eslint": "^9.17.0",
"eslint-plugin-jsdoc": "^50.6.1",
"jsdom": "^25.0.1",
"lerna": "^8.1.9",
"nodemon": "^3.1.9",
"rimraf": "^6.0.1",
Expand Down
28 changes: 11 additions & 17 deletions packages/core/src/Coordinator.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,6 @@ import { voidLogger } from './util/void-logger.js';
import { MosaicClient } from './MosaicClient.js';
import { QueryManager, Priority } from './QueryManager.js';

/**
* @typedef {import('@uwdata/mosaic-sql').Query
* | import('@uwdata/mosaic-sql').DescribeQuery
* | string} QueryType
*/

/**
* The singleton Coordinator instance.
* @type {Coordinator}
Expand Down Expand Up @@ -116,13 +110,13 @@ export class Coordinator {

/**
* Issue a query for which no result (return value) is needed.
* @param {QueryType | QueryType[]} query The query or an array of queries.
* @param { import('./types.js').QueryType[] |
* import('./types.js').QueryType} query The query or an array of queries.
* Each query should be either a Query builder object or a SQL string.
* @param {object} [options] An options object.
* @param {number} [options.priority] The query priority, defaults to
* `Priority.Normal`.
* @returns {QueryResult} A query result
* promise.
* @returns {QueryResult} A query result promise.
*/
exec(query, { priority = Priority.Normal } = {}) {
query = Array.isArray(query) ? query.filter(x => x).join(';\n') : query;
Expand All @@ -132,8 +126,8 @@ export class Coordinator {
/**
* Issue a query to the backing database. The submitted query may be
* consolidate with other queries and its results may be cached.
* @param {QueryType} query The query as either a Query builder object
* or a SQL string.
* @param {import('./types.js').QueryType} query The query as either a Query
* builder object or a SQL string.
* @param {object} [options] An options object.
* @param {'arrow' | 'json'} [options.type] The query result format type.
* @param {boolean} [options.cache=true] If true, cache the query result
Expand All @@ -156,8 +150,8 @@ export class Coordinator {
/**
* Issue a query to prefetch data for later use. The query result is cached
* for efficient future access.
* @param {QueryType} query The query as either a Query builder object
* or a SQL string.
* @param {import('./types.js').QueryType} query The query as either a Query
* builder object or a SQL string.
* @param {object} [options] An options object.
* @param {'arrow' | 'json'} [options.type] The query result format type.
* @returns {QueryResult} A query result promise.
Expand Down Expand Up @@ -196,13 +190,13 @@ export class Coordinator {
* Update client data by submitting the given query and returning the
* data (or error) to the client.
* @param {MosaicClient} client A Mosaic client.
* @param {QueryType} query The data query.
* @param {import('./types.js').QueryType} query The data query.
* @param {number} [priority] The query priority.
* @returns {Promise} A Promise that resolves upon completion of the update.
*/
updateClient(client, query, priority = Priority.Normal) {
client.queryPending();
return this.query(query, { priority })
return client._pending = this.query(query, { priority })
.then(
data => client.queryResult(data).update(),
err => { this._logger.error(err); client.queryError(err); }
Expand All @@ -215,7 +209,7 @@ export class Coordinator {
* the client is simply updated. Otherwise `updateClient` is called. As a
* side effect, this method clears the current preaggregator state.
* @param {MosaicClient} client The client to update.
* @param {QueryType | null} [query] The query to issue.
* @param {import('./types.js').QueryType | null} [query] The query to issue.
*/
requestQuery(client, query) {
this.preaggregator.clear();
Expand All @@ -238,7 +232,7 @@ export class Coordinator {
client.coordinator = this;

// initialize client lifecycle
this.initializeClient(client);
client._pending = this.initializeClient(client);

// connect filter selection
connectSelection(this, client.filterBy, client);
Expand Down
13 changes: 13 additions & 0 deletions packages/core/src/MosaicClient.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Coordinator } from './Coordinator.js';
import { Selection } from './Selection.js';
import { throttle } from './util/throttle.js';

/**
Expand All @@ -11,9 +13,13 @@ export class MosaicClient {
* the client when the selection updates.
*/
constructor(filterSelection) {
/** @type {Selection} */
this._filterBy = filterSelection;
this._requestUpdate = throttle(() => this.requestQuery(), true);
/** @type {Coordinator} */
this._coordinator = null;
/** @type {Promise<any>} */
this._pending = Promise.resolve();
}

/**
Expand All @@ -30,6 +36,13 @@ export class MosaicClient {
this._coordinator = coordinator;
}

/**
* Return a Promise that resolves once the client has updated.
*/
get pending() {
return this._pending;
}

/**
* Return this client's filter selection.
*/
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/QueryManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export class QueryManager {
constructor(
maxConcurrentRequests = 32
) {
/** @type {PriorityQueue} */
this.queue = new PriorityQueue(3);
this.db = null;
this.clientCache = null;
Expand All @@ -18,11 +19,12 @@ export class QueryManager {
this._consolidate = null;
/**
* Requests pending with the query manager.
*
* @type {QueryResult[]}
*/
this.pendingResults = [];
/** @type {number} */
this.maxConcurrentRequests = maxConcurrentRequests;
/** @type {boolean} */
this.pendingExec = false;
}

Expand Down
8 changes: 7 additions & 1 deletion packages/core/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import type { ExprNode } from '@uwdata/mosaic-sql';
import type { DescribeQuery, ExprNode, Query } from '@uwdata/mosaic-sql';

/** Query type accepted by a coordinator. */
export type QueryType =
| string
| Query
| DescribeQuery;

/** String indicating a JavaScript data type. */
export type JSType =
Expand Down
6 changes: 4 additions & 2 deletions packages/core/src/util/throttle.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ const NIL = {};
* a Promise. Upon repeated invocation, the callback will not be invoked
* until a prior Promise resolves. If multiple invocations occurs while
* waiting, only the most recent invocation will be pending.
* @param {(event: *) => Promise} callback The callback function.
* @template E, T
* @param {(event: E) => Promise<T>} callback The callback function.
* @param {boolean} [debounce=true] Flag indicating if invocations
* should also be debounced within the current animation frame.
* @returns A new function that throttles access to the callback.
* @returns {(event: E) => void} A new function that throttles
* access to the callback.
*/
export function throttle(callback, debounce = false) {
let curr;
Expand Down
6 changes: 3 additions & 3 deletions packages/core/test/client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,12 @@ describe('MosaicClient', () => {
}
queryPending() {
// add result promise to global pending queue
this.pending = new QueryResult();
pending.push(this.pending);
this.pendingResult = new QueryResult();
pending.push(this.pendingResult);
}
queryResult(data) {
// fulfill pending promise with sorted data
this.pending.fulfill(
this.pendingResult.fulfill(
data.toArray().sort((a, b) => a.key - b.key)
);
return this;
Expand Down
12 changes: 6 additions & 6 deletions packages/inputs/src/Table.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export class Table extends MosaicClient {

this.offset = 0;
this.limit = +rowBatch;
this.pending = false;
this.isPending = false;

this.selection = as;
this.currentRow = -1;
Expand All @@ -59,15 +59,15 @@ export class Table extends MosaicClient {

let prevScrollTop = -1;
this.element.addEventListener('scroll', evt => {
const { pending, loaded } = this;
const { isPending, loaded } = this;
const { scrollHeight, scrollTop, clientHeight } = evt.target;

const back = scrollTop < prevScrollTop;
prevScrollTop = scrollTop;
if (back || pending || loaded) return;
if (back || isPending || loaded) return;

if (scrollHeight - scrollTop < 2 * clientHeight) {
this.pending = true;
this.isPending = true;
this.requestData(this.offset + this.limit);
}
});
Expand Down Expand Up @@ -168,7 +168,7 @@ export class Table extends MosaicClient {
}

queryResult(data) {
if (!this.pending) {
if (!this.isPending) {
// data is not from an internal request, so reset table
this.loaded = false;
this.data = [];
Expand Down Expand Up @@ -204,7 +204,7 @@ export class Table extends MosaicClient {
this.loaded = true;
}

this.pending = false;
this.isPending = false;
return this;
}

Expand Down
2 changes: 1 addition & 1 deletion packages/plot/src/marks/Mark.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ export function markQuery(channels, table, skip = []) {
if (skip.includes(channel)) continue;

if (channel === 'orderby') {
q.orderby(c.value);
q.orderby(c.value ?? field);
} else if (field) {
if (isAggregateExpression(field)) {
aggr = true;
Expand Down
17 changes: 14 additions & 3 deletions packages/plot/src/plot.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,30 @@ const DEFAULT_ATTRIBUTES = {
};

export class Plot {
/**
* @param {HTMLElement} [element]
*/
constructor(element) {
/** @type {Record<string, any>} */
this.attributes = { ...DEFAULT_ATTRIBUTES };
this.listeners = null;
this.interactors = [];
/** @type {{ legend: import('./legend.js').Legend, include: boolean }[]} */
this.legends = [];
/** @type {import('./marks/Mark.js').Mark[]} */
this.marks = [];
/** @type {Set<import('./marks/Mark.js').Mark> | null} */
this.markset = null;
/** @type {Map<import('@uwdata/mosaic-core').Param, import('./marks/Mark.js').Mark[]>} */
this.params = new Map;
/** @type {ReturnType<synchronizer>} */
this.synch = synchronizer();

/** @type {HTMLElement} */
this.element = element || document.createElement('div');
this.element.setAttribute('class', 'plot');
this.element.style.display = 'flex';
this.element.value = this;
this.params = new Map;
this.synch = synchronizer();
Object.assign(this.element, { value: this });
}

margins() {
Expand Down
Loading
Loading