Repository: GoogleChromeLabs/ps-analysis-tool
Pull request #806 (2024-08-14 12:41:53)

Fixing CLI output typo and improve text
## Description

This pull request corrects some sentences on the output messages from the CLI and improves some sentences to make them clear

## Relevant Technical Choices

<!-- Please describe your changes. -->

## Testing Instructions

1. Open the CLI on the terminal 
2. Run a audit with verbose mode `$ psat https://example.com -v`
3. Check the output messages

## Additional Information:

<!-- Any other information. -->

## Screenshot/Screencast

<!-- Please provide Screenshot/Screencast, if applicable -->

---

## Checklist

<!-- Check these after PR creation -->

- [ ] I have thoroughly tested this code to the best of my abilities.
- [ ] I have reviewed the code myself before requesting a review.
- [ ] This code is covered by unit tests to verify that it works as intended.
- [ ] The QA of this PR is done by a member of the QA team (to be checked by QA).

<!--
Example:

Fixes #123
Partially addresses #22
See #834
-->

Fixes #807



Patch:
diff --git a/packages/analysis-utils/src/browserManagement/index.ts b/packages/analysis-utils/src/browserManagement/index.ts
index e24d30aa..b37a47f9 100644
--- a/packages/analysis-utils/src/browserManagement/index.ts
+++ b/packages/analysis-utils/src/browserManagement/index.ts
@@ -105,7 +105,7 @@ export class BrowserManagement {
       headless: this.isHeadless,
       args,
     });
-    this.debugLog('Browser intialized');
+    this.debugLog('Browser initialized');
   }
 
   async clickOnAcceptBanner(url: string) {
@@ -152,7 +152,7 @@ export class BrowserManagement {
 
   async openPage(): Promise<Page> {
     if (!this.browser) {
-      throw new Error('Browser not intialized');
+      throw new Error('Browser not initialized');
     }
     const sitePage = await this.browser.newPage();
 
@@ -175,7 +175,7 @@ export class BrowserManagement {
     const page = this.pages[url];
 
     if (!page) {
-      throw new Error('no page with the provided id was found');
+      throw new Error('No page with the provided ID was found');
     }
 
     this.debugLog(`Starting navigation to URL: ${url}`);
@@ -195,7 +195,7 @@ export class BrowserManagement {
     const page = this.pages[url];
 
     if (!page) {
-      throw new Error('no page with the provided id was found');
+      throw new Error('No page with the provided ID was found');
     }
 
     try {
@@ -203,7 +203,7 @@ export class BrowserManagement {
         window.scrollBy(0, 10000);
       });
     } catch (error) {
-      this.debugLog('Scrolled to end of page');
+      this.debugLog('Scrolled to the end of page');
       //ignore
     }
 
@@ -480,10 +480,10 @@ export class BrowserManagement {
     const page = this.pages[pageId];
 
     if (!page) {
-      throw new Error(`no page with the provided id was found:${pageId}`);
+      throw new Error(`No page with the provided ID was found:${pageId}`);
     }
 
-    this.debugLog('Attaching network event listeners to page');
+    this.debugLog('Attaching network event listeners to the page');
 
     const cdpSession = await page.createCDPSession();
 
@@ -611,7 +611,7 @@ export class BrowserManagement {
     const page = this.pages[url];
 
     if (!page) {
-      throw new Error('no page with the provided id was found');
+      throw new Error('No page with the provided ID was found');
     }
 
     const domQueryMatches: LibraryData = {};
diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts
index 791fe8ef..77bfaef6 100644
--- a/packages/cli/src/index.ts
+++ b/packages/cli/src/index.ts
@@ -227,7 +227,7 @@ program.parse();
   const cookieDictionary = await fetchDictionary();
 
   spinnies.add('cookie-spinner', {
-    text: 'Analyzing cookies on first site visit',
+    text: 'Analyzing cookies on the first site visit',
   });
 
   const cookieAnalysisAndFetchedResourceData =
@@ -322,12 +322,12 @@ program.parse();
 })().catch((error) => {
   const spinnies = new Spinnies();
   spinnies.add('error-line-1', {
-    text: 'Some error occured while analyzing the website.',
+    text: 'Some errors occurred while analyzing the website.',
     status: 'non-spinnable',
     color: 'red',
   });
   spinnies.add('error-line-2', {
-    text: 'For more information check the stack trace below:\n',
+    text: 'For more information, check the stack trace below:\n',
     status: 'non-spinnable',
     color: 'red',
   });
diff --git a/packages/cli/src/utils/validators.ts b/packages/cli/src/utils/validators.ts
index c62a6cc6..75621489 100644
--- a/packages/cli/src/utils/validators.ts
+++ b/packages/cli/src/utils/validators.ts
@@ -58,11 +58,11 @@ export function numericValidator(value: string, flag: string) {
     switch (flag) {
       case '-n':
         throw new InvalidArgumentError(
-          "Correct value for option  '-n, --number-of-urls <num>' would be non negative number greater than 0 and less than equal to total number of urls"
+          "The correct value for option  '-n, --number-of-urls <num>' would be a nonnegative number greater than 0 and less than equal to the total number of URLs"
         );
       case '-c':
         throw new InvalidArgumentError(
-          "Correct value for option '-c, --concurrency <num>' would be non negative number greater than 0 and less than equal to total number of urls"
+          "The correct value for option '-c, --concurrency <num>' would be a nonnegative number greater than 0 and less than equal to the total number of URLs"
         );
       case '-w':
         throw new InvalidArgumentError(
@@ -75,7 +75,7 @@ export function numericValidator(value: string, flag: string) {
 
   const parsedValue = parseInt(value);
   if (isNaN(parsedValue)) {
-    redLogger(`Error: ${value} is not valid numeric value.`);
+    redLogger(`Error: ${value} is not a valid numeric value.`);
   }
   return parsedValue;
 }
@@ -148,15 +148,15 @@ export function urlValidator(url: string, flag: string) {
     switch (flag) {
       case '[website-url]':
         throw new InvalidArgumentError(
-          'Correct value for command-argument would be https://example.com'
+          'The correct value for the command-argument would be https://example.com'
         );
       case '-u':
         throw new InvalidArgumentError(
-          "Correct value for option '-u, --url <url>' would be https://example.com"
+          "The correct value for option '-u, --url <url>' would be https://example.com"
         );
       case '-s':
         throw new InvalidArgumentError(
-          "Correct value for option '-s, --source-url <url>' would be https://gagan.pro/sitemap/toypta.xml or https://sitemap.superintegratedapp.com/sitemaps/sitemap.csv"
+          "The correct value for option '-s, --source-url <url>' would be https://example.com/sitemap/sitemap.xml or https://example.com/sitemaps/sitemap.csv"
         );
       default:
         throw new InvalidArgumentError('');
@@ -182,7 +182,7 @@ export function outDirValidator(outDir: string, flag: string) {
     switch (flag) {
       case '-o':
         throw new InvalidArgumentError(
-          "Correct value for option '-o, --out-dir <path>' would be /users/path/to/save/output"
+          "The correct value for option '-o, --out-dir <path>' would be /users/path/to/save/output"
         );
       default:
         throw new InvalidArgumentError('');
diff --git a/packages/design-system/src/components/index.ts b/packages/design-system/src/components/index.ts
index e9b5ddf8..063c2a96 100644
--- a/packages/design-system/src/components/index.ts
+++ b/packages/design-system/src/components/index.ts
@@ -52,3 +52,5 @@ export { default as MenuBar } from './menuBar';
 export * from './menuBar';
 export { default as useFiltersMapping } from './cookiesLanding/useFiltersMapping';
 export { default as useGlobalFiltering } from './cookiesLanding/useGlobalFiltering';
+export { default as Tabs } from './tabs';
+export { default as QuickLinksList } from './landingPage/quickLinksList';
diff --git a/packages/design-system/src/components/tabs/index.tsx b/packages/design-system/src/components/tabs/index.tsx
new file mode 100644
index 00000000..5f409134
--- /dev/null
+++ b/packages/design-system/src/components/tabs/index.tsx
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * External dependencies
+ */
+import classNames from 'classnames';
+import React, { useCallback, useState } from 'react';
+
+interface TabsProps {
+  items: Array<{
+    title: string;
+    content: {
+      Element: (props: any) => React.JSX.Element;
+      props?: Record<string, any>;
+    };
+  }>;
+}
+
+const Tabs = ({ items }: TabsProps) => {
+  const [activeTab, setActiveTab] = useState(0);
+
+  const ActiveTabContent = items?.[activeTab].content?.Element;
+
+  const handleKeyDown = useCallback(
+    (event: React.KeyboardEvent<HTMLButtonElement>) => {
+      event.preventDefault();
+
+      if (event.key === 'Tab') {
+        const nextIndex = activeTab + 1;
+        if (nextIndex < items.length) {
+          setActiveTab(nextIndex);
+        } else {
+          setActiveTab(0);
+        }
+      }
+
+      if (event.shiftKey && event.key === 'Tab') {
+        const previousIndex = activeTab - 1;
+        if (previousIndex >= 0) {
+          setActiveTab(previousIndex);
+        } else {
+          setActiveTab(items.length - 1);
+        }
+      }
+    },
+    [activeTab, items.length]
+  );
+
+  return (
+    <div className="max-w-2xl h-fit px-4">
+      <div className="flex gap-10 border-b border-gray-300 dark:border-quartz">
+        {items.map((item, index) => (
+          <button
+            key={index}
+            onClick={() => setActiveTab(index)}
+            onKeyDown={handleKeyDown}
+            autoFocus={index === 0}
+            className={classNames(
+              'pb-1.5 px-3 border-b-2 hover:opacity-80 outline-none text-sm',
+              {
+                'border-bright-navy-blue dark:border-jordy-blue text-bright-navy-blue dark:text-jordy-blue':
+                  index === activeTab,
+              },
+              {
+                'border-transparent text-raisin-black dark:text-bright-gray':
+                  index !== activeTab,
+              }
+            )}
+          >
+            {item.title}
+          </button>
+        ))}
+      </div>
+      <div className="pt-4">
+        {ActiveTabContent && (
+          <ActiveTabContent {...items?.[activeTab].content?.props} />
+        )}
+      </div>
+    </div>
+  );
+};
+
+export default Tabs;
diff --git a/packages/extension/src/view/devtools/components/siteBoundaries/relatedWebsiteSets/index.tsx b/packages/extension/src/view/devtools/components/siteBoundaries/relatedWebsiteSets/index.tsx
index cb4bf84d..ce496a79 100644
--- a/packages/extension/src/view/devtools/components/siteBoundaries/relatedWebsiteSets/index.tsx
+++ b/packages/extension/src/view/devtools/components/siteBoundaries/relatedWebsiteSets/index.tsx
@@ -16,8 +16,13 @@
 /**
  * External dependencies.
  */
-import React from 'react';
-import { LandingPage, PSInfoKey } from '@google-psat/design-system';
+import React, { useMemo } from 'react';
+import {
+  InfoCard,
+  PSInfoKey,
+  QuickLinksList,
+  Tabs,
+} from '@google-psat/design-system';
 import { I18n } from '@google-psat/i18n';
 
 /**
@@ -27,16 +32,44 @@ import RWSJsonGenerator from './jsonGenerator';
 import Insights from './insights';
 
 const RelatedWebsiteSets = () => {
+  const tabItems = useMemo(
+    () => [
+      {
+        title: 'Overview',
+        content: {
+          Element: InfoCard,
+          props: {
+            infoKey: PSInfoKey.RelatedWebsiteSets,
+          },
+        },
+      },
+      {
+        title: 'Membership',
+        content: {
+          Element: Insights,
+        },
+      },
+      {
+        title: 'JSON Generator',
+        content: {
+          Element: RWSJsonGenerator,
+        },
+      },
+    ],
+    []
+  );
+
   return (
     <div data-testid="related-website-sets-content" className="h-full w-full">
-      <LandingPage
-        title={I18n.getMessage('rws')}
-        psInfoKey={PSInfoKey.RelatedWebsiteSets}
-        extraClasses="max-w-2xl h-fit"
-      >
-        <Insights />
-        <RWSJsonGenerator />
-      </LandingPage>
+      <div className="p-4">
+        <div className="flex gap-2 text-2xl font-bold items-baseline text-raisin-black dark:text-bright-gray">
+          <h1 className="text-left">{I18n.getMessage('rws')}</h1>
+        </div>
+      </div>
+      <Tabs items={tabItems} />
+      <div className="mt-8 border-t border-gray-300 dark:border-quartz">
+        <QuickLinksList />
+      </div>
     </div>
   );
 };
diff --git a/packages/extension/src/view/devtools/components/siteBoundaries/relatedWebsiteSets/insights/index.tsx b/packages/extension/src/view/devtools/components/siteBoundaries/relatedWebsiteSets/insights/index.tsx
index d063eec3..5cbfb138 100644
--- a/packages/extension/src/view/devtools/components/siteBoundaries/relatedWebsiteSets/insights/index.tsx
+++ b/packages/extension/src/view/devtools/components/siteBoundaries/relatedWebsiteSets/insights/index.tsx
@@ -88,9 +88,6 @@ const Insights = () => {
         </div>
       ) : (
         <div className="space-y-3">
-          <h3 className="text-xl font-semibold">
-            {I18n.getMessage('membership')}
-          </h3>
           {insightsData?.isURLInRWS ? (
             <>
               <p className="text-lg font-medium">
diff --git a/packages/extension/src/view/devtools/components/siteBoundaries/relatedWebsiteSets/jsonGenerator/components/input/index.tsx b/packages/extension/src/view/devtools/components/siteBoundaries/relatedWebsiteSets/jsonGenerator/components/input/index.tsx
index 03960b2e..88415b29 100644
--- a/packages/extension/src/view/devtools/components/siteBoundaries/relatedWebsiteSets/jsonGenerator/components/input/index.tsx
+++ b/packages/extension/src/view/devtools/components/siteBoundaries/relatedWebsiteSets/jsonGenerator/components/input/index.tsx
@@ -39,7 +39,7 @@ const RWSInput = ({
 }: RWSInputProps) => {
   return (
     <div className="flex flex-col gap">
-      <label className="text-xs">{inputLabel}</label>
+      <label className="text-sm">{inputLabel}</label>
       <input
         type="text"
         className={classNames(
@@ -57,7 +57,7 @@ const RWSInput = ({
         onChange={inputChangeHandler}
       />
       {error && formValidationFailed && (
-        <span className="text-red-500 text-xs mt-2">{error}</span>
+        <span className="text-red-500 text-sm mt-2">{error}</span>
       )}
     </div>
   );
diff --git a/packages/extension/src/view/devtools/components/siteBoundaries/relatedWebsiteSets/jsonGenerator/index.tsx b/packages/extension/src/view/devtools/components/siteBoundaries/relatedWebsiteSets/jsonGenerator/index.tsx
index 7f7e7421..cc14d461 100644
--- a/packages/extension/src/view/devtools/components/siteBoundaries/relatedWebsiteSets/jsonGenerator/index.tsx
+++ b/packages/extension/src/view/devtools/components/siteBoundaries/relatedWebsiteSets/jsonGenerator/index.tsx
@@ -64,13 +64,10 @@ const RWSJsonGenerator = () => {
 
   return (
     <>
-      <div className="overflow-auto py-6">
+      <div className="overflow-auto">
         <div className="text-raisin-black dark:text-bright-gray w-full min-w-[33rem]">
-          <h1 className="text-lg font-semibold">
-            {I18n.getMessage('rwsJsonGenerator')}
-          </h1>
           <p
-            className="text-xs py-3"
+            className="text-sm"
             dangerouslySetInnerHTML={{
               __html: I18n.getMessage('rwsJsonGeneratorNote', [
                 `<a
diff --git a/tailwind.config.cjs b/tailwind.config.cjs
index 29269e5a..4adaa675 100644
--- a/tailwind.config.cjs
+++ b/tailwind.config.cjs
@@ -221,6 +221,7 @@ module.exports = {
       ...colors,
       'american-silver': '#CBCDD1',
       'royal-blue': '#3871E0',
+      'jordy-blue': '#8AB7F8',
       'medium-persian-blue': '#0E639C',
       'bright-navy-blue': '#1A73E8',
       'hex-gray': '#E0E0E0',


Test patch:
diff --git a/packages/design-system/src/components/tabs/tests/index.tsx b/packages/design-system/src/components/tabs/tests/index.tsx
new file mode 100644
index 00000000..2f4e9927
--- /dev/null
+++ b/packages/design-system/src/components/tabs/tests/index.tsx
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * External dependencies
+ */
+import React from 'react';
+import { fireEvent, render, screen } from '@testing-library/react';
+import '@testing-library/jest-dom';
+
+/**
+ * Internal dependencies
+ */
+import Tabs from '..';
+
+describe('Tabs', () => {
+  const props = {
+    items: [
+      {
+        title: 'title1',
+        content: {
+          Element: () => <div>content1</div>,
+        },
+      },
+      {
+        title: 'title2',
+        content: {
+          Element: () => <div>content2</div>,
+        },
+      },
+    ],
+  };
+
+  it('should render', () => {
+    render(<Tabs {...props} />);
+
+    expect(screen.getByText('title1')).toBeInTheDocument();
+    expect(screen.getByText('title1')).toHaveClass('border-b-2');
+
+    fireEvent.click(screen.getByText('title2'));
+
+    expect(screen.getByText('content2')).toBeInTheDocument();
+    expect(screen.getByText('title2')).toHaveClass('border-b-2');
+
+    fireEvent.keyDown(screen.getByText('title2'), { key: 'Tab' });
+
+    expect(screen.getByText('content1')).toBeInTheDocument();
+    expect(screen.getByText('title1')).toHaveClass('border-b-2');
+
+    fireEvent.keyDown(screen.getByText('title1'), {
+      key: 'Tab',
+      shiftKey: true,
+    });
+
+    expect(screen.getByText('content2')).toBeInTheDocument();
+    expect(screen.getByText('title2')).toHaveClass('border-b-2');
+  });
+});
diff --git a/packages/extension/src/view/devtools/components/siteBoundaries/relatedWebsiteSets/jsonGenerator/tests/jsonGenerator.tsx b/packages/extension/src/view/devtools/components/siteBoundaries/relatedWebsiteSets/jsonGenerator/tests/jsonGenerator.tsx
index dd6cfd99..2e969028 100644
--- a/packages/extension/src/view/devtools/components/siteBoundaries/relatedWebsiteSets/jsonGenerator/tests/jsonGenerator.tsx
+++ b/packages/extension/src/view/devtools/components/siteBoundaries/relatedWebsiteSets/jsonGenerator/tests/jsonGenerator.tsx
@@ -62,14 +62,6 @@ describe('RWSJsonGenerator', () => {
     });
   });
 
-  it('should render form', () => {
-    const screen = render(<RWSJsonGenerator open={true} setOpen={noop} />);
-
-    expect(
-      screen.getByText('Related Website Sets JSON Generator')
-    ).toBeInTheDocument();
-  });
-
   it('should interact with contact email and primary domain', async () => {
     const screen = render(<RWSJsonGenerator open={true} setOpen={noop} />);
 
diff --git a/packages/extension/src/view/devtools/e2e-tests/rwsMembership/index.ts b/packages/extension/src/view/devtools/e2e-tests/rwsMembership/index.ts
index 24c389a4..b0fcd34f 100644
--- a/packages/extension/src/view/devtools/e2e-tests/rwsMembership/index.ts
+++ b/packages/extension/src/view/devtools/e2e-tests/rwsMembership/index.ts
@@ -69,6 +69,7 @@ describe('RWS membership', () => {
 
     // Check if RWS is present and click on it.
     await interaction.clickMatchingElement(frame, 'p', 'Related Website Sets');
+    await interaction.clickMatchingElement(frame, 'button', 'Membership');
     await interaction.delay(6000);
 
     const rwsmembershiptext = await interaction.getInnerText(
diff --git a/packages/extension/src/view/devtools/test-utils/puppeteerManagement.ts b/packages/extension/src/view/devtools/test-utils/puppeteerManagement.ts
index 2d830b7e..37f5c099 100644
--- a/packages/extension/src/view/devtools/test-utils/puppeteerManagement.ts
+++ b/packages/extension/src/view/devtools/test-utils/puppeteerManagement.ts
@@ -73,7 +73,7 @@ export class PuppeteerManagement {
 
   async openPage(): Promise<Page> {
     if (!this.browser) {
-      throw new Error('Browser not intialized');
+      throw new Error('Browser not initialized');
     }
     const sitePage = await this.browser.newPage();
     const pages = await this.browser.pages();
@@ -100,11 +100,11 @@ export class PuppeteerManagement {
       return t.type() === 'other' && t.url().startsWith('devtools://');
     });
     if (!devtoolsTargets) {
-      throw new Error('Devtools targets not found.');
+      throw new Error('DevTools targets not found.');
     }
     const devtools = await devtoolsTargets.page();
     if (!devtools) {
-      throw new Error('Devtools not found.');
+      throw new Error('DevTools not found.');
     }
 
     return devtools;