feat: sheet embed support (#1013)

* feat: add basic sheet rendering support

* chore: add missing file

* fix: correct bg colors for none support

* chore: fix test that relied on dark bg

* chore: fix ref

* chore: api spec update

* chore: add todo comments

* chore: use memo

* chore: a bit less verbose

* chore: list

* chore: cleaning

* chore: add rendering test

* chore: enable rendering test

* chore: settings

* chore: settings

* chore: disable rendering tests

* chore: revert test tests
This commit is contained in:
Tobias Åström
2022-12-12 13:54:04 +01:00
committed by GitHub
parent f644c35e1b
commit 3dacac587b
26 changed files with 689 additions and 36 deletions

View File

@@ -110,6 +110,9 @@ jobs:
fi
- store_test_results:
path: coverage/junit
# - run:
# name: Test rendering
# command: yarn run test:rendering
- run:
name: Test component
command: yarn run test:component --chrome.browserWSEndpoint "ws://localhost:3000" --no-launch
@@ -119,6 +122,7 @@ jobs:
- run:
name: Test integration
command: yarn run test:integration --chrome.browserWSEndpoint "ws://localhost:3000" --no-launch
- nebula_create:
project_name: 'generated/hello'
picasso_template: 'none'

1
.gitignore vendored
View File

@@ -2,6 +2,7 @@
*.rej
*.tmp
*.log
*.pem
.cache/
.DS_Store
.idea/

View File

@@ -67,6 +67,7 @@ function createMock(genericObject, options) {
}),
{}
),
genericType: genericObject.type,
};
return { [qId]: mock };
}

View File

@@ -279,7 +279,7 @@ const Cell = forwardRef(
const { nebbie } = halo.public;
const { disableCellPadding = false } = halo.context || {};
const { translator, language, keyboardNavigation } = useContext(InstanceContext);
const { theme: themeName, translator, language, keyboardNavigation } = useContext(InstanceContext);
const theme = useTheme();
const [cellRef, cellRect, cellNode] = useRect();
const [state, dispatch] = useReducer(contentReducer, initialState(initialError));
@@ -289,8 +289,7 @@ const Cell = forwardRef(
const [snOptions, setSnOptions] = useState(initialSnOptions);
const [snPlugins, setSnPlugins] = useState(initialSnPlugins);
const cellElementId = `njs-cell-${currentId}`;
const clickOutElements = [`#${cellElementId}`, '.njs-action-toolbar-popover']; // elements which will not trigger the click out listener
const [selections] = useObjectSelections(app, model, clickOutElements);
const [selections] = useObjectSelections(app, model, [`#${cellElementId}`, '.njs-action-toolbar-popover']); // elements which will not trigger the click out listener
const [hovering, setHover] = useState(false);
const hoveringDebouncer = useRef({ enter: null, leave: null });
const [bgColor, setBgColor] = useState(undefined);
@@ -311,7 +310,7 @@ const Cell = forwardRef(
const bgComp = layout?.components ? layout.components.find((comp) => comp.key === 'general') : null;
setBgColor(resolveBgColor(bgComp, halo.public.theme));
setBgImage(resolveBgImage(bgComp, halo.app));
}, [layout, halo.public.theme, halo.app]);
}, [layout, halo.public.theme, halo.app, themeName]);
focusHandler.current.blurCallback = (resetFocus) => {
halo.root.toggleFocusOfCells();
@@ -495,7 +494,7 @@ const Cell = forwardRef(
width: '100%',
height: '100%',
overflow: 'hidden',
backgroundColor: bgColor,
backgroundColor: bgColor || 'unset',
backgroundImage: bgImage && bgImage.url ? `url(${bgImage.url})` : undefined,
backgroundRepeat: 'no-repeat',
backgroundSize: bgImage && bgImage.size,

View File

@@ -75,6 +75,9 @@ export default function boot({ app, context }) {
addCell(id, cell) {
cells[id] = cell;
},
removeCell(id) {
delete cells[id];
},
add(component) {
(async () => {
await rendered;

View File

@@ -0,0 +1,148 @@
import React, { useEffect, useState, useContext, useMemo } from 'react';
import useLayout from '../hooks/useLayout';
import getObject from '../object/get-object';
import Cell from './Cell';
import uid from '../object/uid';
import { resolveBgColor, resolveBgImage } from '../utils/background-props';
import InstanceContext from '../contexts/InstanceContext';
/**
* @interface
* @extends HTMLElement
* @experimental
* @since 3.1.0
*/
const SheetElement = {
/** @type {'njs-sheet'} */
className: 'njs-sheet',
};
function getCellRenderer(cell, halo, initialSnOptions, initialSnPlugins, initialError, onMount) {
const { x, y, width, height } = cell.bounds;
return (
<div style={{ left: `${x}%`, top: `${y}%`, width: `${width}%`, height: `${height}%`, position: 'absolute' }}>
<Cell
ref={cell.cellRef}
halo={halo}
model={cell.model}
currentId={cell.currentId}
initialSnOptions={initialSnOptions}
initialSnPlugins={initialSnPlugins}
initialError={initialError}
onMount={onMount}
/>
</div>
);
}
function Sheet({ model, halo, initialSnOptions, initialSnPlugins, initialError, onMount }) {
const { root } = halo;
const [layout] = useLayout(model);
const { theme: themeName } = useContext(InstanceContext);
const [cells, setCells] = useState([]);
const [bgColor, setBgColor] = useState(undefined);
const [bgImage, setBgImage] = useState(undefined);
const [deepHash, setDeepHash] = useState('');
/// For each object
useEffect(() => {
if (layout) {
const hash = JSON.stringify(layout.cells);
if (hash === deepHash) {
return;
}
setDeepHash(hash);
const fetchObjects = async () => {
/*
Need to always fetch and evaluate everything as the sheet need to support multiple instances of the same object?
No, there is no way to add the same chart twice, so the optimization should be worth it.
*/
// Clear the cell list
cells.forEach((c) => {
root.removeCell(c.currentId);
});
const lCells = layout.cells;
// TODO - should try reuse existing objects on subsequent renders
// Non-id updates should only change the "css"
const cs = await Promise.all(
lCells.map(async (c) => {
let mounted;
const mountedPromise = new Promise((resolve) => {
mounted = resolve;
});
const cell = cells.find((ce) => ce.id === c.name);
if (cell) {
cell.bounds = c.bounds;
delete cell.mountedPromise;
return cell;
}
const vs = await getObject({ id: c.name }, halo);
return {
model: vs.model,
id: c.name,
bounds: c.bounds,
cellRef: React.createRef(),
currentId: uid(),
mounted,
mountedPromise,
};
})
);
cs.forEach((c) => root.addCell(c.currentId, c.cellRef));
setCells(cs);
};
fetchObjects();
}
}, [layout]);
const cellRenderers = useMemo(
() =>
cells
? cells.map((c) => getCellRenderer(c, halo, initialSnOptions, initialSnPlugins, initialError, c.mounted))
: [],
[cells]
);
useEffect(() => {
const bgComp = layout?.components ? layout.components.find((comp) => comp.key === 'general') : null;
setBgColor(resolveBgColor(bgComp, halo.public.theme));
setBgImage(resolveBgImage(bgComp, halo.app));
}, [layout, halo.public.theme, halo.app, themeName]);
/* TODO
- sheet title + bg + logo etc + as option
- sheet exposed classnames for theming
*/
const height = !layout || Number.isNaN(layout.height) ? '100%' : `${Number(layout.height)}%`;
const promises = cells.map((c) => c.mountedPromise);
const ps = promises.filter((p) => !!p);
if (ps.length) {
Promise.all(promises).then(() => {
// TODO - correct? Currently called each time a new cell is mounted?
onMount();
});
}
return (
<div
className={SheetElement.className}
style={{
width: `100%`,
height,
position: 'relative',
backgroundColor: bgColor,
backgroundImage: bgImage && bgImage.url ? `url(${bgImage.url})` : undefined,
backgroundRepeat: 'no-repeat',
backgroundSize: bgImage && bgImage.size,
backgroundPosition: bgImage && bgImage.pos,
}}
>
{cellRenderers}
</div>
);
}
export default Sheet;

View File

@@ -0,0 +1,31 @@
import React from 'react';
import ReactDOM from 'react-dom';
import Sheet from './Sheet';
export default function glue({ halo, element, model, initialSnOptions, initialSnPlugins, onMount, initialError }) {
const { root } = halo;
const sheetRef = React.createRef();
const portal = ReactDOM.createPortal(
<Sheet
ref={sheetRef}
halo={halo}
model={model}
initialSnOptions={initialSnOptions}
initialSnPlugins={initialSnPlugins}
initialError={initialError}
onMount={onMount}
/>,
element,
model.id
);
const unmount = () => {
root.remove(portal);
model.removeListener('closed', unmount);
};
model.on('closed', unmount);
root.add(portal);
return [unmount, sheetRef];
}

View File

@@ -8,7 +8,7 @@ import AppSelectionsPortal from './components/selections/AppSelections';
import ListBoxPortal from './components/listbox/ListBoxPortal';
import create from './object/create-session-object';
import get from './object/get-object';
import get from './object/get-generic-object';
import flagsFn from './flags/flags';
import { create as typesFn } from './sn/types';
@@ -255,9 +255,10 @@ function nuked(configuration = {}) {
*/
const api = /** @lends Embed# */ {
/**
* Renders a visualization into an HTMLElement.
* Renders a visualization or sheet into an HTMLElement.
* Support for sense sheets is experimental.
* @param {CreateConfig | GetConfig} cfg - The render configuration.
* @returns {Promise<Viz>} A controller to the rendered visualization.
* @returns {Promise<Viz|Sheet>} A controller to the rendered visualization or sheet.
* @example
* // render from existing object
* n.render({

View File

@@ -0,0 +1,35 @@
import init from './initiate';
import initSheet from './initiate-sheet';
import { modelStore, rpcRequestModelStore } from '../stores/model-store';
/**
* @interface BaseConfig
* @description Basic rendering configuration for rendering an object
* @property {HTMLElement} element
* @property {object=} options
* @property {Plugin[]} [plugins]
*/
/**
* @interface GetConfig
* @description Rendering configuration for rendering an existing object
* @extends BaseConfig
* @property {string} id
*/
export default async function getObject({ id, options, plugins, element }, halo) {
const key = `${id}`;
let rpc = rpcRequestModelStore.get(key);
if (!rpc) {
rpc = halo.app.getObject(id);
rpcRequestModelStore.set(key, rpc);
}
const model = await rpc;
modelStore.set(key, model);
if (model.genericType === 'sheet') {
return initSheet(model, { options, plugins, element }, halo);
}
return init(model, { options, plugins, element }, halo);
}

View File

@@ -1,21 +1,6 @@
import init from './initiate';
import { modelStore, rpcRequestModelStore } from '../stores/model-store';
/**
* @interface BaseConfig
* @description Basic rendering configuration for rendering an object
* @property {HTMLElement} element
* @property {object=} options
* @property {Plugin[]} [plugins]
*/
/**
* @interface GetConfig
* @description Rendering configuration for rendering an existing object
* @extends BaseConfig
* @property {string} id
*/
export default async function getObject({ id, options, plugins, element }, halo) {
const key = `${id}`;
let rpc = rpcRequestModelStore.get(key);
@@ -25,5 +10,6 @@ export default async function getObject({ id, options, plugins, element }, halo)
}
const model = await rpc;
modelStore.set(key, model);
return init(model, { options, plugins, element }, halo);
}

View File

@@ -0,0 +1,23 @@
/* eslint no-underscore-dangle:0 */
import sheetAPI from '../sheet';
export default async function initSheet(model, optional, halo, initialError, onDestroy = async () => {}) {
const api = sheetAPI({
model,
halo,
initialError,
onDestroy,
});
if (optional.options) {
api.__DO_NOT_USE__.options(optional.options);
}
if (optional.plugins) {
api.__DO_NOT_USE__.plugins(optional.plugins);
}
if (optional.element) {
await api.__DO_NOT_USE__.mount(optional.element);
}
return api;
}

134
apis/nucleus/src/sheet.js Normal file
View File

@@ -0,0 +1,134 @@
/* eslint-disable no-underscore-dangle */
import glueSheet from './components/sheetGlue';
import validatePlugins from './plugins/plugins';
import getPatches from './utils/patcher';
const noopi = () => {};
export default function sheet({ model, halo, initialError, onDestroy = async () => {} } = {}) {
let unmountSheet = noopi;
let sheetRef = null;
let mountedReference = null;
let onMount = null;
const mounted = new Promise((resolve) => {
onMount = resolve;
});
let initialSnOptions = {};
let initialSnPlugins = [];
const setSnOptions = async (opts) => {
if (mountedReference) {
(async () => {
await mounted;
sheetRef.current.setSnOptions({
...initialSnOptions,
...opts,
});
})();
} else {
// Handle setting options before mount
initialSnOptions = {
...initialSnOptions,
...opts,
};
}
};
const setSnPlugins = async (plugins) => {
validatePlugins(plugins);
if (mountedReference) {
(async () => {
await mounted;
sheetRef.current.setSnPlugins(plugins);
})();
} else {
// Handle setting plugins before mount
initialSnPlugins = plugins;
}
};
/**
* @class
* @alias Sheet
* @classdesc A controller to further modify a visualization after it has been rendered.
* @experimental
* @since 3.1.0
* @example
* const sheet = await embed(app).render({
* element,
* id: "jD5Gd"
* });
* sheet.destroy();
*/
const api = /** @lends Sheet# */ {
/**
* The id of this sheets's generic object.
* @type {string}
*/
id: model.id,
/**
* This sheets Enigma model, a representation of the generic object.
* @type {string}
*/
model,
/**
* Destroys the sheet and removes it from the the DOM.
* @example
* const sheet = await embed(app).render({
* element,
* id: "jD5Gd"
* });
* sheet.destroy();
*/
async destroy() {
await onDestroy();
unmountSheet();
unmountSheet = noopi;
},
// ===== unexposed experimental API - use at own risk ======
__DO_NOT_USE__: {
mount(element) {
if (mountedReference) {
throw new Error('Already mounted');
}
mountedReference = element;
[unmountSheet, sheetRef] = glueSheet({
halo,
element,
model,
initialSnOptions,
initialSnPlugins,
initialError,
onMount,
});
return mounted;
},
async applyProperties(props) {
const current = await model.getEffectiveProperties();
const patches = getPatches('/', props, current);
if (patches.length) {
return model.applyPatches(patches, true);
}
return undefined;
},
options(opts) {
setSnOptions(opts);
},
plugins(plugins) {
setSnPlugins(plugins);
},
exportImage() {
throw new Error('Not implemented');
},
takeSnapshot() {
throw new Error('Not implemented');
},
getModel() {
return model;
},
},
};
return api;
}

View File

@@ -62,7 +62,7 @@ export const resolveBgImage = (bgComp, app) => {
if (bgImageDef) {
let url = '';
if (bgImageDef.mode === 'media') {
if (bgImageDef.mode === 'media' || bgComp.useImage === 'media') {
url = bgImageDef?.mediaUrl?.qStaticContentUrl?.qUrl
? decodeURIComponent(bgImageDef.mediaUrl.qStaticContentUrl.qUrl)
: undefined;
@@ -85,7 +85,7 @@ export const resolveBgColor = (bgComp, theme) => {
if (bgColor.useColorExpression) {
return theme.validateColor(bgColor.colorExpression);
}
return bgColor.color && bgColor.color.color !== 'none' ? theme.getColorPickerColor(bgColor.color) : undefined;
return bgColor.color && bgColor.color.color !== 'none' ? theme.getColorPickerColor(bgColor.color, true) : undefined;
}
return undefined;
};

View File

@@ -66,6 +66,11 @@ export default function viz({ model, halo, initialError, onDestroy = async () =>
* @type {string}
*/
id: model.id,
/**
* This visualizations Enigma model, a representation of the generic object.
* @type {string}
*/
model,
/**
* Destroys the visualization and removes it from the the DOM.
* @example

View File

@@ -648,7 +648,7 @@
},
"entries": {
"render": {
"description": "Renders a visualization into an HTMLElement.",
"description": "Renders a visualization or sheet into an HTMLElement.\nSupport for sense sheets is experimental.",
"kind": "function",
"params": [
{
@@ -666,11 +666,19 @@
}
],
"returns": {
"description": "A controller to the rendered visualization.",
"description": "A controller to the rendered visualization or sheet.",
"type": "Promise",
"generics": [
{
"type": "#/definitions/Viz"
"kind": "union",
"items": [
{
"type": "#/definitions/Viz"
},
{
"type": "#/definitions/Sheet"
}
]
}
]
},
@@ -991,6 +999,39 @@
}
}
},
"Sheet": {
"description": "A controller to further modify a visualization after it has been rendered.",
"stability": "experimental",
"availability": {
"since": "3.1.0"
},
"kind": "class",
"constructor": {
"kind": "function",
"params": []
},
"entries": {
"id": {
"description": "The id of this sheets's generic object.",
"type": "string"
},
"model": {
"description": "This sheets Enigma model, a representation of the generic object.",
"type": "string"
},
"destroy": {
"description": "Destroys the sheet and removes it from the the DOM.",
"kind": "function",
"params": [],
"examples": [
"const sheet = await embed(app).render({\n element,\n id: \"jD5Gd\"\n});\nsheet.destroy();"
]
}
},
"examples": [
"const sheet = await embed(app).render({\n element,\n id: \"jD5Gd\"\n});\nsheet.destroy();"
]
},
"Viz": {
"description": "A controller to further modify a visualization after it has been rendered.",
"kind": "class",
@@ -1003,6 +1044,10 @@
"description": "The id of this visualization's generic object.",
"type": "string"
},
"model": {
"description": "This visualizations Enigma model, a representation of the generic object.",
"type": "string"
},
"destroy": {
"description": "Destroys the visualization and removes it from the the DOM.",
"kind": "function",
@@ -2117,6 +2162,12 @@
"type": "string"
}
}
},
{
"name": "supportNone",
"description": "Shifts the palette index by one to account for the \"none\" color",
"optional": true,
"type": "boolean"
}
],
"returns": {
@@ -2124,7 +2175,7 @@
"type": "string"
},
"examples": [
"theme.getColorPickerColor({ index: 1 });\ntheme.getColorPickerColor({ color: 'red' });"
"theme.getColorPickerColor({ index: 1 });\ntheme.getColorPickerColor({ index: 1 }, true);\ntheme.getColorPickerColor({ color: 'red' });"
]
},
"getContrastingColorTo": {

View File

@@ -213,10 +213,11 @@ declare namespace stardust {
constructor();
/**
* Renders a visualization into an HTMLElement.
* Renders a visualization or sheet into an HTMLElement.
* Support for sense sheets is experimental.
* @param cfg The render configuration.
*/
render(cfg: stardust.CreateConfig | stardust.GetConfig): Promise<stardust.Viz>;
render(cfg: stardust.CreateConfig | stardust.GetConfig): Promise<stardust.Viz | stardust.Sheet>;
/**
* Updates the current context of this embed instance.
@@ -294,6 +295,23 @@ declare namespace stardust {
qId: string;
}
/**
* A controller to further modify a visualization after it has been rendered.
*/
class Sheet {
constructor();
id: string;
model: string;
/**
* Destroys the sheet and removes it from the the DOM.
*/
destroy(): void;
}
/**
* A controller to further modify a visualization after it has been rendered.
*/
@@ -302,6 +320,8 @@ declare namespace stardust {
id: string;
model: string;
/**
* Destroys the visualization and removes it from the the DOM.
*/
@@ -618,11 +638,12 @@ declare namespace stardust {
/**
* Resolve a color object using the color picker palette from the provided JSON theme.
* @param c
* @param supportNone Shifts the palette index by one to account for the "none" color
*/
getColorPickerColor(c: {
index?: number;
color?: string;
}): string;
}, supportNone?: boolean): string;
/**
* Get the best contrasting color against the specified `color`.

View File

@@ -49,10 +49,12 @@ export default function theme() {
* @param {object} c
* @param {number=} c.index
* @param {string=} c.color
* @param {boolean=} supportNone Shifts the palette index by one to account for the "none" color
* @returns {string} The resolved color.
*
* @example
* theme.getColorPickerColor({ index: 1 });
* theme.getColorPickerColor({ index: 1 }, true);
* theme.getColorPickerColor({ color: 'red' });
*/
getColorPickerColor(...a) {

View File

@@ -75,7 +75,9 @@ export default function theme(resolvedTheme) {
others: resolvedTheme.dataColors.othersColor,
};
},
uiColor(c) {
uiColor(c, shift) {
// eslint-disable-next-line no-param-reassign
shift = !!shift;
if (c.index < 0 || typeof c.index === 'undefined') {
return c.color;
}
@@ -85,10 +87,10 @@ export default function theme(resolvedTheme) {
if (!uiPalette) {
return c.color;
}
if (typeof uiPalette.colors[c.index] === 'undefined') {
if (typeof uiPalette.colors[c.index - shift] === 'undefined') {
return c.color;
}
return uiPalette.colors[c.index];
return uiPalette.colors[c.index - shift];
},
};
}

View File

@@ -1,4 +1,4 @@
const { execSync } = require('child_process');
const cmd = 'mocha test/rendering/listbox/listbox.spec.js --bail false --timeout 30000';
const cmd = 'mocha test/rendering/**/*.spec.js --bail false --timeout 30000';
execSync(cmd, { stdio: 'inherit' });

View File

@@ -18,6 +18,7 @@
position: absolute;
width: 100%;
height: 100%;
background-color: rgb(50, 50, 50);
}
</style>
</head>

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

View File

@@ -0,0 +1,45 @@
const pie = {
component: {
mounted(el) {
// eslint-disable-next-line
el.innerHTML = '<div id="pie" style="background: aliceblue; height:100%; width:100%;">Hello pie</div>';
},
},
};
const bar = function (env) {
env.translator.add({
id: 'hello',
locale: {
'sv-SE': 'Hej {0}!',
},
});
return {
component: {
mounted(el) {
// eslint-disable-next-line
el.innerHTML = `<div id="bar" style="font-size: 64px; background: tan; height:100%; width:100%;">${env.translator.get(
'hello',
['bar']
)}</div>`;
},
},
};
};
// eslint-disable-next-line
const configured = stardust.embed.createConfiguration({
context: {
language: 'sv-SE',
},
types: [
{
name: 'piechart',
load: () => Promise.resolve(pie),
},
{
name: 'barchart',
load: () => Promise.resolve(bar),
},
],
});

View File

@@ -0,0 +1,50 @@
/* eslint arrow-body-style: 0 */
window.getFuncs = function getFuncs() {
return {
getSheetLayout: () => {
return {
qInfo: {
qId: 'sheet',
},
visualization: 'sheet',
cells: [
{
name: 'bar',
bounds: {
x: 0,
y: 0,
width: 50,
height: 50,
},
},
{
name: 'pie',
bounds: {
x: 50,
y: 50,
width: 50,
height: 50,
},
},
],
};
},
getBarLayout: () => {
return {
qInfo: {
qId: 'bar',
},
visualization: 'barchart',
};
},
getPieLayout: () => {
return {
qInfo: {
qId: 'pie',
},
visualization: 'piechart',
};
},
};
};

View File

@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<script src="/apis/stardust/dist/stardust.js"></script>
<script src="configured.js"></script>
<script src="sheet-data.js"></script>
<script defer src="sheet.js"></script>
<style>
html,
body {
margin: 0;
padding: 0;
overflow: hidden;
}
#object {
position: absolute;
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="object" data-type="sheet"></div>
</body>
</html>

View File

@@ -0,0 +1,46 @@
/* global configured */
/* eslint no-underscore-dangle: 0 */
(() => {
async function getMocks(EnigmaMocker) {
const { getSheetLayout, getBarLayout, getPieLayout } = window.getFuncs();
const obj = [
{
id: `sheet`,
type: 'sheet',
getLayout: () => getSheetLayout(),
},
{
id: `bar`,
type: 'barchart',
getLayout: () => getBarLayout(),
},
{
id: `pie`,
type: 'piechart',
getLayout: () => getPieLayout(),
},
];
const app = await EnigmaMocker.fromGenericObjects(obj);
return {
obj,
app,
};
}
const init = async () => {
const element = document.querySelector('#object');
const { app } = await getMocks(window.stardust.EnigmaMocker);
const nebbie = configured(app);
const inst = await nebbie.render({ id: 'sheet', element });
return () => {
inst?.unmount(element);
};
};
return init();
})();

View File

@@ -0,0 +1,36 @@
const getPage = require('../setup');
const startServer = require('../server');
const { looksLike } = require('../testUtils');
describe('listbox mashup rendering test', () => {
const object = '[data-type="sheet"]';
let page;
let takeScreenshot;
let destroyServer;
let destroyBrowser;
let url;
const PAGE_OPTIONS = { width: 600, height: 500 };
beforeEach(async () => {
({ url, destroy: destroyServer } = await startServer());
({ page, takeScreenshot, destroy: destroyBrowser } = await getPage(PAGE_OPTIONS));
});
afterEach(async () => {
await Promise.all([destroyServer(), destroyBrowser()]);
});
it('selecting two values should result in two green rows', async () => {
const FILE_NAME = 'sheet_basic.png';
await page.goto(`${url}/sheet/sheet.html`);
await page.waitForSelector(object, { visible: true });
const snapshotElement = await page.$(object);
await page.$('#bar');
await page.$('#pie');
const { path: capturedPath } = await takeScreenshot(FILE_NAME, snapshotElement);
await looksLike(FILE_NAME, capturedPath);
});
});