Repository: opossum-tool/OpossumUI (ts)

Issue:
Project statistics popup should ignore hidden signals
**Is your feature request related to a problem? Please describe.**
User can hide signals by right-click -> hide. This should be reflected at the Project statistics popup.

**Describe the solution you'd like**
Do not take into account hidden signals at all the data at Project statistics popup -> in this case numbers should decrease.

**Describe alternatives you've considered**
Maybe introduce one more column "hidden signals" or something like that.

**Additional context**
![image](https://github.com/opossum-tool/OpossumUI/assets/85183359/27af8304-56c7-470a-9c53-f45e3c1bcf24)


Pull request:
feat: ignore hidden signals in project statistics
### Summary of changes

If a user hides a signal this signal will no longer be taken into account for the project statistics popup. I also changed the displayed icon in resource tree.  If all critical signals linked to a resource are hidden the resource no longer has the criticality icon next to it. If in general all signals linked to the resource are hidden, the flag indicating  that the resource contains signals is no longer shown.

### Context and reason for change

fixes #2227

### How can the changes be tested

Play around with hiding critical signals and look at the project statistics and the respective icons in the resource tree.


Patch:
diff --git a/package.json b/package.json
index 7a36cdfd..2b34a520 100644
--- a/package.json
+++ b/package.json
@@ -13,7 +13,7 @@
     "@mui/material": "^5.15.15",
     "@mui/system": "^5.15.15",
     "@reduxjs/toolkit": "^2.2.3",
-    "@tanstack/react-query": "^5.31.0",
+    "@tanstack/react-query": "^5.29.2",
     "compare-versions": "^6.1.0",
     "dayjs": "^1.11.10",
     "electron-log": "^5.1.2",
@@ -26,7 +26,7 @@
     "object-hash": "^3.0.0",
     "packageurl-js": "^1.2.1",
     "proxy-memoize": "^2.0.5",
-    "re-resizable": "^6.9.14",
+    "re-resizable": "^6.9.11",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
     "react-error-boundary": "^4.0.13",
@@ -34,7 +34,7 @@
     "react-hotkeys-hook": "^4.5.0",
     "react-redux": "^9.1.1",
     "react-virtuoso": "4.7.6",
-    "recharts": "^2.12.6",
+    "recharts": "^2.12.5",
     "spdx-license-ids": "^3.0.17",
     "stream-json": "^1.8.0",
     "upath": "^2.0.1",
diff --git a/src/Frontend/Components/CriticalLicensesTable/CriticalLicensesTable.tsx b/src/Frontend/Components/CriticalLicensesTable/CriticalLicensesTable.tsx
index 3399e422..6680686e 100644
--- a/src/Frontend/Components/CriticalLicensesTable/CriticalLicensesTable.tsx
+++ b/src/Frontend/Components/CriticalLicensesTable/CriticalLicensesTable.tsx
@@ -6,16 +6,16 @@ import { sortBy } from 'lodash';
 import { ReactElement } from 'react';
 
 import { Criticality } from '../../../shared/shared-types';
+import { text } from '../../../shared/text';
 import { clickableIcon } from '../../shared-styles';
 import { LicenseNamesWithCriticality } from '../../types/types';
 import { ProjectLicensesTable } from '../ProjectLicensesTable/ProjectLicensesTable';
 
 const LICENSE_COLUMN_NAME_IN_TABLE = 'License name';
-const COUNT_COLUMN_NAME_IN_TABLE = 'Signals Count';
 const FOOTER_TITLE = 'Total';
 const TABLE_COLUMN_NAMES = [
   LICENSE_COLUMN_NAME_IN_TABLE,
-  COUNT_COLUMN_NAME_IN_TABLE,
+  text.projectStatisticsPopup.criticalLicensesSignalCountColumnName,
 ];
 
 const classes = {
@@ -93,7 +93,9 @@ export function CriticalLicensesTable(
           ({ licenseName, totalNumberOfAttributions }) => [
             licenseName,
             {
-              [COUNT_COLUMN_NAME_IN_TABLE]: totalNumberOfAttributions,
+              [text.projectStatisticsPopup
+                .criticalLicensesSignalCountColumnName]:
+                totalNumberOfAttributions,
             },
           ],
         ),
diff --git a/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.tsx b/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.tsx
index db1e4544..c55ebebb 100644
--- a/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.tsx
+++ b/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.tsx
@@ -11,9 +11,9 @@ import { ProjectStatisticsPopupTitle } from '../../enums/enums';
 import { closePopup } from '../../state/actions/view-actions/view-actions';
 import { useAppDispatch, useAppSelector } from '../../state/hooks';
 import {
-  getExternalAttributions,
   getExternalAttributionSources,
   getManualAttributions,
+  getUnresolvedExternalAttributions,
 } from '../../state/selectors/resource-selectors';
 import { useUserSetting } from '../../state/variables/use-user-setting';
 import { AccordionWithPieChart } from '../AccordionWithPieChart/AccordionWithPieChart';
@@ -41,15 +41,19 @@ export function ProjectStatisticsPopup(): ReactElement {
   const dispatch = useAppDispatch();
 
   const manualAttributions = useAppSelector(getManualAttributions);
-  const externalAttributions = useAppSelector(getExternalAttributions);
   const attributionSources = useAppSelector(getExternalAttributionSources);
 
-  const strippedLicenseNameToAttribution =
-    getUniqueLicenseNameToAttribution(externalAttributions);
+  const unresolvedExternalAttribution = useAppSelector(
+    getUnresolvedExternalAttributions,
+  );
+
+  const strippedLicenseNameToAttribution = getUniqueLicenseNameToAttribution(
+    unresolvedExternalAttribution,
+  );
 
   const { licenseCounts, licenseNamesWithCriticality } =
     aggregateLicensesAndSourcesFromAttributions(
-      externalAttributions,
+      unresolvedExternalAttribution,
       strippedLicenseNameToAttribution,
       attributionSources,
     );
diff --git a/src/Frontend/Components/ResourceBrowser/ResourcesTree/ResourcesTreeNode/ResourcesTreeNode.tsx b/src/Frontend/Components/ResourceBrowser/ResourcesTree/ResourcesTreeNode/ResourcesTreeNode.tsx
index 570d771c..5c1d93ac 100644
--- a/src/Frontend/Components/ResourceBrowser/ResourcesTree/ResourcesTreeNode/ResourcesTreeNode.tsx
+++ b/src/Frontend/Components/ResourceBrowser/ResourcesTree/ResourcesTreeNode/ResourcesTreeNode.tsx
@@ -86,6 +86,7 @@ export function ResourcesTreeNode({ node, nodeId, nodeName }: TreeNode) {
         nodeId,
         resourcesToExternalAttributions,
         externalAttributions,
+        resolvedExternalAttributions,
       )}
       isAttributionBreakpoint={attributionBreakpoints.has(nodeId)}
       showFolderIcon={canHaveChildren && !filesWithChildren.has(nodeId)}
diff --git a/src/Frontend/Components/ResourceBrowser/ResourcesTree/ResourcesTreeNode/ResourcesTreeNode.util.ts b/src/Frontend/Components/ResourceBrowser/ResourcesTree/ResourcesTreeNode/ResourcesTreeNode.util.ts
index 18fa095e..caf63410 100644
--- a/src/Frontend/Components/ResourceBrowser/ResourcesTree/ResourcesTreeNode/ResourcesTreeNode.util.ts
+++ b/src/Frontend/Components/ResourceBrowser/ResourcesTree/ResourcesTreeNode/ResourcesTreeNode.util.ts
@@ -16,11 +16,19 @@ export function getCriticality(
   nodeId: string,
   resourcesToExternalAttributions: ResourcesToAttributions,
   externalAttributions: Attributions,
+  resolvedExternalAttributions: Set<string>,
 ): Criticality | undefined {
-  if (hasExternalAttribution(nodeId, resourcesToExternalAttributions)) {
+  if (
+    hasUnresolvedExternalAttribution(
+      nodeId,
+      resourcesToExternalAttributions,
+      resolvedExternalAttributions,
+    )
+  ) {
     const attributionsForResource = resourcesToExternalAttributions[nodeId];
     for (const attributionId of attributionsForResource) {
       if (
+        !resolvedExternalAttributions.has(attributionId) &&
         externalAttributions[attributionId].criticality === Criticality.High
       ) {
         return Criticality.High;
@@ -29,6 +37,7 @@ export function getCriticality(
 
     for (const attributionId of attributionsForResource) {
       if (
+        !resolvedExternalAttributions.has(attributionId) &&
         externalAttributions[attributionId].criticality === Criticality.Medium
       ) {
         return Criticality.Medium;
diff --git a/src/Frontend/Components/ResourceBrowser/ResourcesTree/ResourcesTreeNode/ResourcesTreeNodeLabel/ResourcesTreeNodeLabel.tsx b/src/Frontend/Components/ResourceBrowser/ResourcesTree/ResourcesTreeNode/ResourcesTreeNodeLabel/ResourcesTreeNodeLabel.tsx
index 12e137bc..50fdd7b6 100644
--- a/src/Frontend/Components/ResourceBrowser/ResourcesTree/ResourcesTreeNode/ResourcesTreeNodeLabel/ResourcesTreeNodeLabel.tsx
+++ b/src/Frontend/Components/ResourceBrowser/ResourcesTree/ResourcesTreeNode/ResourcesTreeNodeLabel/ResourcesTreeNodeLabel.tsx
@@ -93,7 +93,7 @@ export function ResourcesTreeNodeLabel(props: Props): ReactElement {
       >
         {props.labelText}
       </MuiTypography>
-      {props.hasExternalAttribution &&
+      {props.hasUnresolvedExternalAttribution &&
         (props.criticality ? (
           <CriticalityIcon
             criticality={props.criticality}
diff --git a/src/Frontend/state/selectors/resource-selectors.ts b/src/Frontend/state/selectors/resource-selectors.ts
index 386cc97d..4f4dd7b9 100644
--- a/src/Frontend/state/selectors/resource-selectors.ts
+++ b/src/Frontend/state/selectors/resource-selectors.ts
@@ -50,6 +50,22 @@ export function getResolvedExternalAttributions(state: State): Set<string> {
   return state.resourceState.resolvedExternalAttributions;
 }
 
+export function getUnresolvedExternalAttributions(state: State): Attributions {
+  let unresolvedExternalAttribution = {};
+  for (const [attributionId, packageInfo] of Object.entries(
+    state.resourceState.externalData.attributions,
+  )) {
+    if (!state.resourceState.resolvedExternalAttributions.has(attributionId)) {
+      unresolvedExternalAttribution = {
+        ...unresolvedExternalAttribution,
+        [attributionId]: packageInfo,
+      };
+    }
+  }
+
+  return unresolvedExternalAttribution;
+}
+
 export function getExpandedIds(state: State): Array<string> {
   return state.resourceState.expandedIds;
 }
diff --git a/src/shared/text.ts b/src/shared/text.ts
index b35d6a64..02151661 100644
--- a/src/shared/text.ts
+++ b/src/shared/text.ts
@@ -102,6 +102,7 @@ export const text = {
   projectStatisticsPopup: {
     title: 'Project Statistics',
     toggleStartupCheckbox: 'Show project statistics on startup',
+    criticalLicensesSignalCountColumnName: 'Signals Count',
   },
   unsavedChangesPopup: {
     title: 'Unsaved Changes',
diff --git a/yarn.lock b/yarn.lock
index 302c29cb..62819aeb 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3233,21 +3233,21 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@tanstack/query-core@npm:5.31.0":
-  version: 5.31.0
-  resolution: "@tanstack/query-core@npm:5.31.0"
-  checksum: 10/8c3a67af76d2317039c7d37c139e3bb431a6cc0a3fb07b66ea318a6af48161f2821bed9afffe72a1219dbef23fc813d0fca0fdebdea8bd7ba136829126884977
+"@tanstack/query-core@npm:5.29.0":
+  version: 5.29.0
+  resolution: "@tanstack/query-core@npm:5.29.0"
+  checksum: 10/a58f1f899292daea66bf8341a4e6b8f96064c0a45bbcb350d68b004b871c6d2894dcecb268389695b093167b0a0acb6bcf5d01f52a335ffb128348b6705450b1
   languageName: node
   linkType: hard
 
-"@tanstack/react-query@npm:^5.31.0":
-  version: 5.31.0
-  resolution: "@tanstack/react-query@npm:5.31.0"
+"@tanstack/react-query@npm:^5.29.2":
+  version: 5.29.2
+  resolution: "@tanstack/react-query@npm:5.29.2"
   dependencies:
-    "@tanstack/query-core": "npm:5.31.0"
+    "@tanstack/query-core": "npm:5.29.0"
   peerDependencies:
     react: ^18.0.0
-  checksum: 10/7d4b594b9db629666551375df6db61d38966a06ee28d75164dfa87d5b1b327436110535e4f1f24748fd98057948baf4334154db15ff1fa0da24795b552abfbae
+  checksum: 10/fff25a36d764b0bec7d4ec3f499a74881abf043e81a8f97c71ffffd52fb6b1cfd60f0199ac4150902beccc95e80b7c32ec3a56ae2ee07bdd388e14bb4546123b
   languageName: node
   linkType: hard
 
@@ -9794,7 +9794,7 @@ __metadata:
     "@playwright/test": "npm:^1.43.1"
     "@reduxjs/toolkit": "npm:^2.2.3"
     "@tanstack/eslint-plugin-query": "npm:^5.28.11"
-    "@tanstack/react-query": "npm:^5.31.0"
+    "@tanstack/react-query": "npm:^5.29.2"
     "@testing-library/dom": "npm:^10.0.0"
     "@testing-library/jest-dom": "npm:^6.4.2"
     "@testing-library/react": "npm:^15.0.2"
@@ -9853,7 +9853,7 @@ __metadata:
     prettier: "npm:^3.2.5"
     prettier-plugin-sh: "npm:^0.14.0"
     proxy-memoize: "npm:^2.0.5"
-    re-resizable: "npm:^6.9.14"
+    re-resizable: "npm:^6.9.11"
     react: "npm:^18.2.0"
     react-dom: "npm:^18.2.0"
     react-error-boundary: "npm:^4.0.13"
@@ -9861,7 +9861,7 @@ __metadata:
     react-hotkeys-hook: "npm:^4.5.0"
     react-redux: "npm:^9.1.1"
     react-virtuoso: "npm:4.7.6"
-    recharts: "npm:^2.12.6"
+    recharts: "npm:^2.12.5"
     run-script-os: "npm:^1.1.6"
     spdx-license-ids: "npm:^3.0.17"
     start-server-and-test: "npm:^2.0.3"
@@ -10381,13 +10381,13 @@ __metadata:
   languageName: node
   linkType: hard
 
-"re-resizable@npm:^6.9.14":
-  version: 6.9.14
-  resolution: "re-resizable@npm:6.9.14"
+"re-resizable@npm:^6.9.11":
+  version: 6.9.11
+  resolution: "re-resizable@npm:6.9.11"
   peerDependencies:
     react: ^16.13.1 || ^17.0.0 || ^18.0.0
     react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0
-  checksum: 10/eb3c40ead7f0db3bac6971cc15efd660ed403018db4a44e084392f248199244a6400ff3f741b2a1aa897bb5ede498d9e72c3d580a6f3acf06819639a0d943d26
+  checksum: 10/3f2590374d03a6b5f7ebc26f273bdb461ceecb1322adf3b7925c695b1c6fa0841225feb79cf63d19c3c2451a9a8ed4f4a82fa119f3a22565ce78e0619d2b624c
   languageName: node
   linkType: hard
 
@@ -10591,9 +10591,9 @@ __metadata:
   languageName: node
   linkType: hard
 
-"recharts@npm:^2.12.6":
-  version: 2.12.6
-  resolution: "recharts@npm:2.12.6"
+"recharts@npm:^2.12.5":
+  version: 2.12.5
+  resolution: "recharts@npm:2.12.5"
   dependencies:
     clsx: "npm:^2.0.0"
     eventemitter3: "npm:^4.0.1"
@@ -10606,7 +10606,7 @@ __metadata:
   peerDependencies:
     react: ^16.0.0 || ^17.0.0 || ^18.0.0
     react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0
-  checksum: 10/afb92147c7b1f7e1ed2f4ee3fb5ae868aab6cc32409f3b29a02b4c08e1933b161b4d226b78c87dbf3c956f07f9936f35dfeca6c3f3bc888e3a591ccd04124d29
+  checksum: 10/c4da741e16ea0f49e88f060bc27fa38797aca5fb46910bec7d899271648c26cd8c320bc4516d40823cd1ae948551ec19eba1e8108598f2b9cceb353b61e5a7db
   languageName: node
   linkType: hard
 


Test patch:
diff --git a/src/Frontend/Components/ResourceBrowser/ResourcesTree/ResourcesTreeNode/ResourcesTreeNodeLabel/__tests__/ResourcesTreeNodeLabel.test.tsx b/src/Frontend/Components/ResourceBrowser/ResourcesTree/ResourcesTreeNode/ResourcesTreeNodeLabel/__tests__/ResourcesTreeNodeLabel.test.tsx
index 0751ab22..1e45d43e 100644
--- a/src/Frontend/Components/ResourceBrowser/ResourcesTree/ResourcesTreeNode/ResourcesTreeNodeLabel/__tests__/ResourcesTreeNodeLabel.test.tsx
+++ b/src/Frontend/Components/ResourceBrowser/ResourcesTree/ResourcesTreeNode/ResourcesTreeNodeLabel/__tests__/ResourcesTreeNodeLabel.test.tsx
@@ -80,7 +80,7 @@ describe('ResourcesTreeNodeLabel', () => {
     ).toBeInTheDocument();
   });
 
-  it('renders a folder with resolved signal and icon', () => {
+  it('renders a folder with resolved signal but without criticality icon', () => {
     render(
       <ResourcesTreeNodeLabel
         labelText={'Test label'}
@@ -99,7 +99,6 @@ describe('ResourcesTreeNodeLabel', () => {
     );
 
     expect(screen.getByText('Test label')).toBeInTheDocument();
-    expect(screen.getByLabelText('Criticality icon')).toBeInTheDocument();
     expect(
       screen.getByLabelText('Directory icon without information'),
     ).toBeInTheDocument();
diff --git a/src/Frontend/Components/ResourceBrowser/ResourcesTree/ResourcesTreeNode/__tests__/ResourcesTreeNode.util.test.ts b/src/Frontend/Components/ResourceBrowser/ResourcesTree/ResourcesTreeNode/__tests__/ResourcesTreeNode.util.test.ts
index 3f7a0fc7..9e508f8f 100644
--- a/src/Frontend/Components/ResourceBrowser/ResourcesTree/ResourcesTreeNode/__tests__/ResourcesTreeNode.util.test.ts
+++ b/src/Frontend/Components/ResourceBrowser/ResourcesTree/ResourcesTreeNode/__tests__/ResourcesTreeNode.util.test.ts
@@ -38,6 +38,7 @@ describe('ResourcesTreeNode', () => {
         nodeId,
         resourcesToExternalAttributions,
         externalAttributions,
+        new Set(),
       );
       expect(criticality).toEqual(expectedCriticalities[nodeId]);
     }
diff --git a/src/Frontend/Components/ResourceBrowser/ResourcesTree/__tests__/ResourcesTree.test.tsx b/src/Frontend/Components/ResourceBrowser/ResourcesTree/__tests__/ResourcesTree.test.tsx
index e14ec6b1..83a425bd 100644
--- a/src/Frontend/Components/ResourceBrowser/ResourcesTree/__tests__/ResourcesTree.test.tsx
+++ b/src/Frontend/Components/ResourceBrowser/ResourcesTree/__tests__/ResourcesTree.test.tsx
@@ -206,7 +206,7 @@ describe('ResourcesTree', () => {
     act(() => {
       store.dispatch(addResolvedExternalAttributions([testUuid]));
     });
-    expectIconToExist(screen, 'Criticality icon', 'src', true);
+    expectIconToExist(screen, 'Criticality icon', 'src', false);
     expectResourceIconLabelToBe(
       screen,
       'src',
diff --git a/src/e2e-tests/__tests__/project-statistics.test.ts b/src/e2e-tests/__tests__/project-statistics.test.ts
new file mode 100644
index 00000000..d1f91b57
--- /dev/null
+++ b/src/e2e-tests/__tests__/project-statistics.test.ts
@@ -0,0 +1,71 @@
+// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
+// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
+//
+// SPDX-License-Identifier: Apache-2.0
+import { Criticality } from '../../shared/shared-types';
+import { faker, test } from '../utils';
+
+const [resourceName1] = faker.opossum.resourceNames({ count: 1 });
+const [attributionId1, packageInfo1] = faker.opossum.rawAttribution({
+  criticality: Criticality.Medium,
+});
+const [attributionId3, packageInfo3] = faker.opossum.rawAttribution({
+  criticality: Criticality.Medium,
+});
+test.use({
+  data: {
+    inputData: faker.opossum.inputData({
+      resources: faker.opossum.resources({
+        [resourceName1]: 1,
+      }),
+      externalAttributions: faker.opossum.rawAttributions({
+        [attributionId1]: packageInfo1,
+        [attributionId3]: packageInfo3,
+      }),
+      resourcesToAttributions: faker.opossum.resourcesToAttributions({
+        [faker.opossum.filePath(resourceName1)]: [
+          attributionId1,
+          attributionId3,
+        ],
+      }),
+    }),
+    outputData: faker.opossum.outputData({}),
+  },
+});
+
+test('opens, displays, and closes project statistics', async ({
+  menuBar,
+  projectStatisticsPopup,
+}) => {
+  await menuBar.openProjectStatistics();
+  await projectStatisticsPopup.assert.titleIsVisible();
+
+  await projectStatisticsPopup.closeButton.click();
+  await projectStatisticsPopup.assert.titleIsHidden();
+});
+
+test('hidden signals are ignored for project statistics', async ({
+  menuBar,
+  projectStatisticsPopup,
+  resourcesTree,
+  signalsPanel,
+  attributionDetails,
+}) => {
+  await menuBar.openProjectStatistics();
+  await projectStatisticsPopup.assert.titleIsVisible();
+
+  await projectStatisticsPopup.assert.criticalLicenseCount(2);
+
+  await projectStatisticsPopup.closeButton.click();
+  await resourcesTree.goto(resourceName1);
+
+  await signalsPanel.packageCard.assert.isVisible(packageInfo1);
+  await signalsPanel.packageCard.assert.isVisible(packageInfo3);
+
+  await signalsPanel.packageCard.click(packageInfo3);
+  await attributionDetails.deleteButton.click();
+  await signalsPanel.packageCard.assert.isHidden(packageInfo3);
+
+  await menuBar.openProjectStatistics();
+  await projectStatisticsPopup.assert.criticalLicenseCount(1);
+});
diff --git a/src/e2e-tests/page-objects/ProjectStatisticsPopup.ts b/src/e2e-tests/page-objects/ProjectStatisticsPopup.ts
index 13aadaa9..624bdfbc 100644
--- a/src/e2e-tests/page-objects/ProjectStatisticsPopup.ts
+++ b/src/e2e-tests/page-objects/ProjectStatisticsPopup.ts
@@ -4,15 +4,28 @@
 // SPDX-License-Identifier: Apache-2.0
 import { expect, type Locator, type Page } from '@playwright/test';
 
+import { text } from '../../shared/text';
+
 export class ProjectStatisticsPopup {
   private readonly node: Locator;
   readonly title: Locator;
   readonly closeButton: Locator;
+  readonly totalCriticalLicensesCount: Locator;
 
   constructor(window: Page) {
     this.node = window.getByLabel('project statistics');
-    this.title = this.node.getByText('Project Statistics');
+    this.title = this.node.getByRole('heading').getByText('Project Statistics');
     this.closeButton = this.node.getByRole('button', { name: 'Close' });
+    const signalsCount = window.getByText(
+      text.projectStatisticsPopup.criticalLicensesSignalCountColumnName,
+    );
+    this.totalCriticalLicensesCount = this.node
+      .getByRole('table')
+      .filter({ has: signalsCount })
+      .getByRole('row')
+      .last()
+      .getByRole('cell')
+      .last();
   }
 
   public assert = {
@@ -22,5 +35,10 @@ export class ProjectStatisticsPopup {
     titleIsHidden: async (): Promise<void> => {
       await expect(this.title).toBeHidden();
     },
+    criticalLicenseCount: async (count: number): Promise<void> => {
+      await expect(this.totalCriticalLicensesCount).toContainText(
+        count.toString(),
+      );
+    },
   };
 }