diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7492c3264..626f90b08 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,9 @@ jobs: validate: runs-on: ubuntu-latest needs: build + permissions: + contents: read + checks: write steps: - uses: actions/checkout@v6 - name: Enable Corepack @@ -58,14 +61,26 @@ jobs: JEST_JUNIT_OUTPUT_DIR: ./coverage/junit/ run: yarn test:unit --coverage - name: Store junit results + if: always() uses: actions/upload-artifact@v7 with: name: junit path: coverage/junit + - name: Publish junit test report + if: ${{ always() && hashFiles('coverage/junit/*.xml') != '' }} + uses: dorny/test-reporter@v2 + with: + name: Unit Tests + path: coverage/junit/*.xml + reporter: jest-junit + fail-on-error: false test-integration: runs-on: ubuntu-latest needs: build + permissions: + contents: read + checks: write steps: - uses: actions/checkout@v6 - name: Enable Corepack @@ -84,7 +99,17 @@ jobs: - name: Install Playwright Browsers run: npx playwright install --with-deps - name: Test component + env: + PLAYWRIGHT_JUNIT_OUTPUT_NAME: component-junit.xml run: yarn run test:component + - name: Publish component test report + if: ${{ always() && hashFiles('test-results/component-junit.xml') != '' }} + uses: dorny/test-reporter@v2 + with: + name: Component Tests + path: test-results/component-junit.xml + reporter: jest-junit + fail-on-error: false - name: Test mashup run: yarn run test:mashup - name: Store mashup artifacts @@ -93,8 +118,26 @@ jobs: with: name: mashup path: ./test/mashup + - name: Publish mashup test report + if: ${{ always() && hashFiles('test/mashup/reports/xml/*.xml') != '' }} + uses: dorny/test-reporter@v2 + with: + name: Mashup Tests + path: test/mashup/reports/xml/*.xml + reporter: jest-junit + fail-on-error: false - name: Test integration + env: + PLAYWRIGHT_JUNIT_OUTPUT_NAME: integration-junit.xml run: yarn run test:integration + - name: Publish integration test report + if: ${{ always() && hashFiles('test-results/integration-junit.xml') != '' }} + uses: dorny/test-reporter@v2 + with: + name: Integration Tests + path: test-results/integration-junit.xml + reporter: jest-junit + fail-on-error: false - name: Test rendering run: yarn run test:rendering - name: Store rendering artifacts @@ -103,6 +146,14 @@ jobs: with: name: rendering path: ./test/rendering + - name: Publish rendering test report + if: ${{ always() && hashFiles('test/rendering/reports/xml/*.xml') != '' }} + uses: dorny/test-reporter@v2 + with: + name: Rendering Tests + path: test/rendering/reports/xml/*.xml + reporter: jest-junit + fail-on-error: false test-create: runs-on: ubuntu-latest diff --git a/apis/nucleus/package.json b/apis/nucleus/package.json index 4ac6145df..4db17a903 100644 --- a/apis/nucleus/package.json +++ b/apis/nucleus/package.json @@ -6,14 +6,14 @@ "devDependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", - "@mui/icons-material": "^6.5.0", - "@mui/material": "^6.5.0", + "@mui/icons-material": "^7.3.9", + "@mui/material": "^7.3.9", "@nebula.js/conversion": "^6.7.0", "@nebula.js/locale": "^6.7.0", "@nebula.js/supernova": "^6.7.0", "@nebula.js/theme": "^6.7.0", "@nebula.js/ui": "^6.7.0", - "@qlik/api": "2.4.1", + "@qlik/api": "2.5.0", "@testing-library/dom": "^10.4.1", "@testing-library/react": "^16.3.2", "extend": "3.0.2", diff --git a/apis/nucleus/src/components/ActionsToolbar.jsx b/apis/nucleus/src/components/ActionsToolbar.jsx index 2f960c4fa..398581d1b 100644 --- a/apis/nucleus/src/components/ActionsToolbar.jsx +++ b/apis/nucleus/src/components/ActionsToolbar.jsx @@ -38,9 +38,9 @@ const ActionToolbarElement = { const ActionsGroup = React.forwardRef(({ className, actions = [], addAnchor = false, isRtl = false }, ref) => actions.length > 0 ? ( - + {actions.map((e, ix) => ( - + ))} @@ -192,7 +192,7 @@ function ActionsToolbar({ /> )} {showDivider && ( - + )} diff --git a/apis/nucleus/src/components/Cell.jsx b/apis/nucleus/src/components/Cell.jsx index b825728db..9f0928fff 100644 --- a/apis/nucleus/src/components/Cell.jsx +++ b/apis/nucleus/src/components/Cell.jsx @@ -142,9 +142,10 @@ const handleModal = ({ sn, layout, model }) => { }; const filterData = (d) => (d.qError ? d.qError.qErrorCode === 7005 : true); +const hasError = (e) => e && e.qError && e.qError.qErrorCode !== 7005; const validateInfo = (min, info, getDescription, translatedError, translatedCalcCond) => - [...Array(min).keys()].map((i) => { + [...Array(Math.max(min, info.length)).keys()].map((i) => { const exists = !!(info && info[i]); const softError = exists && info[i].qError && info[i].qError.qErrorCode === 7005; const error = exists && !softError && info[i].qError; @@ -173,14 +174,14 @@ const validateTarget = (translator, layout, properties, def) => { const reqDimErrors = validateInfo( minD, getInfo(c.qDimensionInfo || c.qItems), - (i) => def.dimensions.description(properties, i), + (i) => def.dimensions.description(properties, i, {}), translator.get('Visualization.Invalid.Dimension'), translator.get('Visualization.UnfulfilledCalculationCondition') ); const reqMeasErrors = validateInfo( minM, getInfo(c.qMeasureInfo), - (i) => def.measures.description(properties, i), + (i) => def.measures.description(properties, i, {}), translator.get('Visualization.Invalid.Measure'), translator.get('Visualization.UnfulfilledCalculationCondition') ); @@ -197,18 +198,29 @@ const validateCubes = (translator, targets, layout) => { let hasLayoutErrors = false; let hasLayoutUnfulfilledCalculcationCondition = false; const layoutErrors = []; + for (let i = 0; i < targets.length; ++i) { const def = targets[i]; + const minD = def.dimensions.min(); const minM = def.measures.min(); + const c = def.resolveLayout(layout); - const d = getInfo(c.qDimensionInfo || c.qItems).filter(filterData); // Filter out optional calc conditions - const m = getInfo(c.qMeasureInfo).filter(filterData); // Filter out optional calc conditions + + const dims = getInfo(c.qDimensionInfo || c.qItems); + const meas = getInfo(c.qMeasureInfo); + + const d = dims.filter(filterData); // Filter out optional calc conditions + const m = meas.filter(filterData); // Filter out optional calc conditions aggMinD += minD; aggMinM += minM; if (d.length < minD || m.length < minM) { hasUnfulfilledErrors = true; } + + // Check for all non-calc-cond errors + hasUnfulfilledErrors = hasUnfulfilledErrors || dims.some(hasError) || meas.some(hasError); + if (c.qError) { hasLayoutErrors = true; hasLayoutUnfulfilledCalculcationCondition = c.qError.qErrorCode === 7005; @@ -683,8 +695,7 @@ const Cell = forwardRef( ); return ( - - {WrappedIcon} - + + {WrappedIcon} + {d.description} @@ -47,14 +47,13 @@ function DescriptionRow({ d }) { function Descriptions({ data }) { const theme = useTheme(); return ( - + {data.map((e, ix) => { const Rows = e.descriptions.map((d, dix) => ); return ( Rows.length > 0 && ( - + - + {title} - + {message} diff --git a/apis/nucleus/src/components/Footer.jsx b/apis/nucleus/src/components/Footer.jsx index 301ef7491..e985dbce9 100644 --- a/apis/nucleus/src/components/Footer.jsx +++ b/apis/nucleus/src/components/Footer.jsx @@ -40,7 +40,6 @@ function Footer({ layout, titleStyles = {}, translator, flags, isCardTheme, isRt return layout && layout.showTitles && (layout.footnote || showFilters) ? ( (themePaddingEnabled ? paddingTop : theme.spacing(1)) }} diff --git a/apis/nucleus/src/components/Header.jsx b/apis/nucleus/src/components/Header.jsx index 3c90b7dbf..6c16e8f9f 100644 --- a/apis/nucleus/src/components/Header.jsx +++ b/apis/nucleus/src/components/Header.jsx @@ -78,8 +78,8 @@ function Header({ id, layout, sn, anchorEl, hovering, focusHandler, titleStyles /> ); return ( - - + + {showTitle ? ( @@ -109,7 +109,7 @@ function Header({ id, layout, sn, anchorEl, hovering, focusHandler, titleStyles )} - {Toolbar} + {Toolbar} ); } diff --git a/apis/nucleus/src/components/LongRunningQuery.jsx b/apis/nucleus/src/components/LongRunningQuery.jsx index f3967f2c9..49757ad59 100644 --- a/apis/nucleus/src/components/LongRunningQuery.jsx +++ b/apis/nucleus/src/components/LongRunningQuery.jsx @@ -33,17 +33,17 @@ const StyledGrid = styled(Grid)(() => ({ export function Cancel({ cancel, translator, ...props }) { return ( <> - - + + - + {translator.get('Object.Update.Active')} - + @@ -55,15 +55,15 @@ export function Cancel({ cancel, translator, ...props }) { export function Retry({ retry, translator, ...props }) { return ( <> - + - + {translator.get('Object.Update.Cancelled')} - + diff --git a/apis/nucleus/src/components/__tests__/cell.test.jsx b/apis/nucleus/src/components/__tests__/cell.test.jsx index dcd3238f1..a48b98f27 100644 --- a/apis/nucleus/src/components/__tests__/cell.test.jsx +++ b/apis/nucleus/src/components/__tests__/cell.test.jsx @@ -395,6 +395,98 @@ describe('', () => { expect(ftypes[0].props.title).toBe('Visualization.Incomplete'); }); + test('should not render requirements', async () => { + const localLayout = { visualization: 'sn', title: 'foo', foo: { qDimensionInfo: [{}], qMeasureInfo: [{}] } }; + const sn = { + generator: { + qae: { + data: { + targets: [ + { + resolveLayout: () => localLayout.foo, + dimensions: { + min: () => 1, + max: () => 1, + description: (_properties, ix) => + ix === 0 + ? 'Column' + : 'Cells - dkslfjd dkslfjd dkslfjd dkslfjd dkslfjd dkslfjd dkslfjd dkslfjd dkslfjd dkslfjd dkslfjd dkslfjd dkslfjd dkslfjd ', + }, + measures: { + min: () => 1, + max: () => 1, + description: () => 'Size', + }, + }, + ], + }, + }, + }, + }; + const types = { + get: jest.fn().mockReturnValue({ + supernova: async () => ({ create: () => sn }), + }), + getSupportedVersion: jest.fn().mockReturnValue('1.0.0'), + }; + const model = { + getProperties: async () => {}, + }; + await render({ types, model }); + + const ftypes = renderer.root.findAllByType(CError); + expect(ftypes).toHaveLength(0); + const renderedSn = renderer.root.findByType(Supernova); + expect(renderedSn.props.sn).toEqual(sn); + }); + + test('should render requirements for dim error', async () => { + const localLayout = { + visualization: 'sn', + foo: { qDimensionInfo: [{}, { qError: { qErrorCode: 7000 } }], qMeasureInfo: [{}] }, + }; + const sn = { + generator: { + qae: { + data: { + targets: [ + { + resolveLayout: () => localLayout.foo, + dimensions: { + min: () => 1, + max: () => 2, + description: (_properties, ix) => + ix === 0 + ? 'Column' + : 'Cells - dkslfjd dkslfjd dkslfjd dkslfjd dkslfjd dkslfjd dkslfjd dkslfjd dkslfjd dkslfjd dkslfjd dkslfjd dkslfjd dkslfjd ', + }, + measures: { + min: () => 1, + max: () => 1, + description: () => 'Size', + }, + }, + ], + }, + }, + }, + }; + const types = { + get: jest.fn().mockReturnValue({ + supernova: async () => ({ create: () => sn }), + }), + getSupportedVersion: jest.fn().mockReturnValue('1.0.0'), + }; + const model = { + getProperties: async () => {}, + }; + await render({ types, model }); + + const ftypes = renderer.root.findAllByType(CError); + expect(ftypes).toHaveLength(1); + expect(ftypes[0].props.title).toBe('Visualization.Incomplete'); + }); + test('should render requirements for multiple cubes', async () => { const localLayout = { visualization: 'sn', foo: { qDimensionInfo: [], qMeasureInfo: [] } }; const sn = { diff --git a/apis/nucleus/src/components/listbox/ListBoxInline.jsx b/apis/nucleus/src/components/listbox/ListBoxInline.jsx index 64cc85b78..c72d9ea69 100644 --- a/apis/nucleus/src/components/listbox/ListBoxInline.jsx +++ b/apis/nucleus/src/components/listbox/ListBoxInline.jsx @@ -294,7 +294,6 @@ function ListBoxInline({ options, layout }) { /> {showAttachedToolbar && listBoxHeader} - + - + {isInvalid ? ( ) : ( diff --git a/apis/nucleus/src/components/listbox/ListBoxPopover.jsx b/apis/nucleus/src/components/listbox/ListBoxPopover.jsx index c2a74e875..6ec0e2a4a 100644 --- a/apis/nucleus/src/components/listbox/ListBoxPopover.jsx +++ b/apis/nucleus/src/components/listbox/ListBoxPopover.jsx @@ -157,8 +157,8 @@ export default function ListBoxPopover({ direction={direction} > - - + + {isLocked ? ( )} - - + + - +
- + )} {showLeftIcon && ( - + {lockedIconComp || (showSearchIcon && searchIconComp)} )} @@ -262,9 +261,7 @@ export default function ListBoxHeader({ {layout.title} - - {actionsToolbar} - + {actionsToolbar} ); } diff --git a/apis/nucleus/src/components/listbox/components/ListBoxRowColumn/ListBoxRowColumn.jsx b/apis/nucleus/src/components/listbox/components/ListBoxRowColumn/ListBoxRowColumn.jsx index e6cac5c94..48e41a7ae 100644 --- a/apis/nucleus/src/components/listbox/components/ListBoxRowColumn/ListBoxRowColumn.jsx +++ b/apis/nucleus/src/components/listbox/components/ListBoxRowColumn/ListBoxRowColumn.jsx @@ -222,10 +222,7 @@ function RowColumn({ index, rowIndex, columnIndex, style, data }) { layoutOrder={layoutOrder} itemPadding={itemPadding} gap={0} - className={joinClassNames(['value', ...classArr])} - classes={{ - root: classes.fieldRoot, - }} + className={joinClassNames(['value', classes.fieldRoot, ...classArr])} onClick={onClick} onMouseDown={onMouseDown} onMouseUp={onMouseUp} @@ -248,12 +245,7 @@ function RowColumn({ index, rowIndex, columnIndex, style, data }) { frequencyMax={frequencyMax} /> )} - + {labels ? ( } {showAnyIcon && ( - + {showLockIcon && } {showTickIcon && } diff --git a/apis/nucleus/src/components/listbox/components/ListBoxRowColumn/components/Frequency.jsx b/apis/nucleus/src/components/listbox/components/ListBoxRowColumn/components/Frequency.jsx index 5f7e0f93e..ffe28fbf7 100644 --- a/apis/nucleus/src/components/listbox/components/ListBoxRowColumn/components/Frequency.jsx +++ b/apis/nucleus/src/components/listbox/components/ListBoxRowColumn/components/Frequency.jsx @@ -8,7 +8,6 @@ function Frequency({ cell, checkboxes, dense, showGray }) { const frequencyText = getFrequencyText(cell?.qFrequency); return (