diff --git a/.circleci/config.yml b/.circleci/config.yml index 8bfbff86d..cdb4ace11 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -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' diff --git a/.gitignore b/.gitignore index 794b7ead1..6ad681ab9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ *.rej *.tmp *.log +*.pem .cache/ .DS_Store .idea/ diff --git a/apis/enigma-mocker/src/mocks/get-object-mock.js b/apis/enigma-mocker/src/mocks/get-object-mock.js index cad65eccc..d6352818f 100644 --- a/apis/enigma-mocker/src/mocks/get-object-mock.js +++ b/apis/enigma-mocker/src/mocks/get-object-mock.js @@ -67,6 +67,7 @@ function createMock(genericObject, options) { }), {} ), + genericType: genericObject.type, }; return { [qId]: mock }; } diff --git a/apis/nucleus/src/components/Cell.jsx b/apis/nucleus/src/components/Cell.jsx index 7523a5500..670edde61 100644 --- a/apis/nucleus/src/components/Cell.jsx +++ b/apis/nucleus/src/components/Cell.jsx @@ -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, diff --git a/apis/nucleus/src/components/NebulaApp.jsx b/apis/nucleus/src/components/NebulaApp.jsx index 134b3c2bc..e7fa22b51 100644 --- a/apis/nucleus/src/components/NebulaApp.jsx +++ b/apis/nucleus/src/components/NebulaApp.jsx @@ -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; diff --git a/apis/nucleus/src/components/Sheet.jsx b/apis/nucleus/src/components/Sheet.jsx new file mode 100644 index 000000000..673c7ac69 --- /dev/null +++ b/apis/nucleus/src/components/Sheet.jsx @@ -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 ( +
+ +
+ ); +} + +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 ( +
+ {cellRenderers} +
+ ); +} + +export default Sheet; diff --git a/apis/nucleus/src/components/sheetGlue.jsx b/apis/nucleus/src/components/sheetGlue.jsx new file mode 100644 index 000000000..ce9ae0543 --- /dev/null +++ b/apis/nucleus/src/components/sheetGlue.jsx @@ -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( + , + element, + model.id + ); + + const unmount = () => { + root.remove(portal); + model.removeListener('closed', unmount); + }; + + model.on('closed', unmount); + + root.add(portal); + return [unmount, sheetRef]; +} diff --git a/apis/nucleus/src/index.js b/apis/nucleus/src/index.js index 8ca7905ae..ae947b43d 100644 --- a/apis/nucleus/src/index.js +++ b/apis/nucleus/src/index.js @@ -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} A controller to the rendered visualization. + * @returns {Promise} A controller to the rendered visualization or sheet. * @example * // render from existing object * n.render({ diff --git a/apis/nucleus/src/object/get-generic-object.js b/apis/nucleus/src/object/get-generic-object.js new file mode 100644 index 000000000..17e4b9390 --- /dev/null +++ b/apis/nucleus/src/object/get-generic-object.js @@ -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); +} diff --git a/apis/nucleus/src/object/get-object.js b/apis/nucleus/src/object/get-object.js index d9e9f172c..f5cb486fa 100644 --- a/apis/nucleus/src/object/get-object.js +++ b/apis/nucleus/src/object/get-object.js @@ -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); } diff --git a/apis/nucleus/src/object/initiate-sheet.js b/apis/nucleus/src/object/initiate-sheet.js new file mode 100644 index 000000000..dbfc6f03c --- /dev/null +++ b/apis/nucleus/src/object/initiate-sheet.js @@ -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; +} diff --git a/apis/nucleus/src/sheet.js b/apis/nucleus/src/sheet.js new file mode 100644 index 000000000..0f6ef7818 --- /dev/null +++ b/apis/nucleus/src/sheet.js @@ -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; +} diff --git a/apis/nucleus/src/utils/background-props.js b/apis/nucleus/src/utils/background-props.js index 57b0bf25e..30127b8c7 100644 --- a/apis/nucleus/src/utils/background-props.js +++ b/apis/nucleus/src/utils/background-props.js @@ -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; }; diff --git a/apis/nucleus/src/viz.js b/apis/nucleus/src/viz.js index 14e7f4023..b7949e562 100644 --- a/apis/nucleus/src/viz.js +++ b/apis/nucleus/src/viz.js @@ -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 diff --git a/apis/stardust/api-spec/spec.json b/apis/stardust/api-spec/spec.json index 4e3413035..ed8b0c1ac 100644 --- a/apis/stardust/api-spec/spec.json +++ b/apis/stardust/api-spec/spec.json @@ -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": { diff --git a/apis/stardust/types/index.d.ts b/apis/stardust/types/index.d.ts index 9ffc2348f..cb015ea21 100644 --- a/apis/stardust/types/index.d.ts +++ b/apis/stardust/types/index.d.ts @@ -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; + render(cfg: stardust.CreateConfig | stardust.GetConfig): Promise; /** * 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`. diff --git a/apis/theme/src/index.js b/apis/theme/src/index.js index 8085126cb..c5362b2d4 100644 --- a/apis/theme/src/index.js +++ b/apis/theme/src/index.js @@ -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) { diff --git a/apis/theme/src/palette-resolver.js b/apis/theme/src/palette-resolver.js index 0e5fe632c..26fb7c9ab 100644 --- a/apis/theme/src/palette-resolver.js +++ b/apis/theme/src/palette-resolver.js @@ -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]; }, }; } diff --git a/scripts/run-rendering-tests.js b/scripts/run-rendering-tests.js index 919c40811..ac32bbcae 100644 --- a/scripts/run-rendering-tests.js +++ b/scripts/run-rendering-tests.js @@ -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' }); diff --git a/test/mashup/snaps/single.html b/test/mashup/snaps/single.html index f89d14894..7bc23bbdc 100644 --- a/test/mashup/snaps/single.html +++ b/test/mashup/snaps/single.html @@ -18,6 +18,7 @@ position: absolute; width: 100%; height: 100%; + background-color: rgb(50, 50, 50); } diff --git a/test/rendering/__artifacts__/baseline/sheet_basic.png b/test/rendering/__artifacts__/baseline/sheet_basic.png new file mode 100644 index 000000000..2d87bae97 Binary files /dev/null and b/test/rendering/__artifacts__/baseline/sheet_basic.png differ diff --git a/test/rendering/sheet/configured.js b/test/rendering/sheet/configured.js new file mode 100644 index 000000000..6d97db151 --- /dev/null +++ b/test/rendering/sheet/configured.js @@ -0,0 +1,45 @@ +const pie = { + component: { + mounted(el) { + // eslint-disable-next-line + el.innerHTML = '
Hello pie
'; + }, + }, +}; + +const bar = function (env) { + env.translator.add({ + id: 'hello', + locale: { + 'sv-SE': 'Hej {0}!', + }, + }); + return { + component: { + mounted(el) { + // eslint-disable-next-line + el.innerHTML = `
${env.translator.get( + 'hello', + ['bar'] + )}
`; + }, + }, + }; +}; + +// 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), + }, + ], +}); diff --git a/test/rendering/sheet/sheet-data.js b/test/rendering/sheet/sheet-data.js new file mode 100644 index 000000000..0eae89c61 --- /dev/null +++ b/test/rendering/sheet/sheet-data.js @@ -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', + }; + }, + }; +}; diff --git a/test/rendering/sheet/sheet.html b/test/rendering/sheet/sheet.html new file mode 100644 index 000000000..695985351 --- /dev/null +++ b/test/rendering/sheet/sheet.html @@ -0,0 +1,28 @@ + + + + + + + + + + + + +
+ + diff --git a/test/rendering/sheet/sheet.js b/test/rendering/sheet/sheet.js new file mode 100644 index 000000000..4b3742a0c --- /dev/null +++ b/test/rendering/sheet/sheet.js @@ -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(); +})(); diff --git a/test/rendering/sheet/sheet.spec.js b/test/rendering/sheet/sheet.spec.js new file mode 100644 index 000000000..f3454aaa0 --- /dev/null +++ b/test/rendering/sheet/sheet.spec.js @@ -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); + }); +});