diff --git a/packages/extension/src/manifest.json b/packages/extension/src/manifest.json index dc63ffc90..bb570be3f 100644 --- a/packages/extension/src/manifest.json +++ b/packages/extension/src/manifest.json @@ -1,5 +1,5 @@ { - "name": "Privacy Analysis Tool", + "name": "Privacy Sandbox Analysis Tool", "description": "Tooling for understanding cookie usage and guidance on new privacy-preserving Chrome APIs.", "version": "0.1.0", "manifest_version": 3, diff --git a/packages/extension/src/view/design-system/components/button/test/index.tsx b/packages/extension/src/view/design-system/components/button/tests/index.tsx similarity index 100% rename from packages/extension/src/view/design-system/components/button/test/index.tsx rename to packages/extension/src/view/design-system/components/button/tests/index.tsx diff --git a/packages/extension/src/view/design-system/components/index.ts b/packages/extension/src/view/design-system/components/index.ts index b2b090713..0a3386a45 100644 --- a/packages/extension/src/view/design-system/components/index.ts +++ b/packages/extension/src/view/design-system/components/index.ts @@ -20,5 +20,6 @@ export { default as MatrixComponentHorizontal } from './matrix/matrixComponent/m export { default as Matrix } from './matrix'; export { default as MessageBox } from './messageBox'; export { default as Button } from './button'; +export { default as ProgressBar } from './progressBar'; export type { MatrixComponentProps } from './matrix/matrixComponent'; diff --git a/packages/extension/src/view/design-system/components/messageBox/test/index.tsx b/packages/extension/src/view/design-system/components/messageBox/test/index.tsx deleted file mode 100644 index d195d4fa8..000000000 --- a/packages/extension/src/view/design-system/components/messageBox/test/index.tsx +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2023 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 { render, screen } from '@testing-library/react'; -import '@testing-library/jest-dom/extend-expect'; - -/** - * Internal dependencies. - */ -import MessageBox from '..'; - -describe('MessageBox', () => { - it('renders the headerText and bodyText', () => { - render( - - ); - expect( - screen.getByText('No cookies found on this page') - ).toBeInTheDocument(); - expect( - screen.getByText('Please try refreshing this page') - ).toBeInTheDocument(); - }); -}); diff --git a/packages/extension/src/view/devtools/app.tsx b/packages/extension/src/view/devtools/app.tsx index d346035ab..95815e326 100644 --- a/packages/extension/src/view/devtools/app.tsx +++ b/packages/extension/src/view/devtools/app.tsx @@ -26,7 +26,7 @@ import './app.css'; import TABS from './tabs'; import { Sidebar } from './components'; import { useCookieStore } from './stateProviders/syncCookieStore'; -import { Button } from '../design-system/components'; +import { Button, ProgressBar } from '../design-system/components'; const App: React.FC = () => { const [selectedTabIndex, setSelectedTabIndex] = useState(0); @@ -35,14 +35,30 @@ const App: React.FC = () => { returningToSingleTab, changeListeningToThisTab, allowedNumberOfTabs, + loading, } = useCookieStore(({ state, actions }) => ({ isCurrentTabBeingListenedTo: state.isCurrentTabBeingListenedTo, returningToSingleTab: state.returningToSingleTab, changeListeningToThisTab: actions.changeListeningToThisTab, allowedNumberOfTabs: state.allowedNumberOfTabs, + loading: state.loading, })); const TabContent = TABS[selectedTabIndex].component; + if ( + loading || + (loading && + isCurrentTabBeingListenedTo && + allowedNumberOfTabs && + allowedNumberOfTabs === 'single') + ) { + return ( +
+ +
+ ); + } + if ( (isCurrentTabBeingListenedTo && allowedNumberOfTabs && diff --git a/packages/extension/src/view/devtools/components/sidebar/index.tsx b/packages/extension/src/view/devtools/components/sidebar/index.tsx index 80203cd67..1ff2b4e58 100644 --- a/packages/extension/src/view/devtools/components/sidebar/index.tsx +++ b/packages/extension/src/view/devtools/components/sidebar/index.tsx @@ -249,9 +249,6 @@ const Sidebar: React.FC = ({ selectedIndex, setIndex }) => { accordionState && accordionState[id] )} index={index} - isCookiesTabOpen={Boolean( - accordionState && accordionState['cookies'] - )} isAccordionHeaderSelected={ selectedIndex === index && !selectedFrame } diff --git a/packages/extension/src/view/devtools/stateProviders/syncCookieStore/index.tsx b/packages/extension/src/view/devtools/stateProviders/syncCookieStore/index.tsx index 16b6dc515..5945d196b 100644 --- a/packages/extension/src/view/devtools/stateProviders/syncCookieStore/index.tsx +++ b/packages/extension/src/view/devtools/stateProviders/syncCookieStore/index.tsx @@ -22,6 +22,7 @@ import React, { useEffect, useState, useCallback, + useRef, } from 'react'; /** @@ -37,6 +38,7 @@ export interface CookieStoreContext { state: { tabCookies: TabCookies | null; tabUrl: string | null; + loading: boolean; tabFrames: TabFrames | null; selectedFrame: string | null; returningToSingleTab: boolean; @@ -55,6 +57,7 @@ const initialState: CookieStoreContext = { tabUrl: null, tabFrames: null, selectedFrame: null, + loading: true, isCurrentTabBeingListenedTo: false, returningToSingleTab: false, allowedNumberOfTabs: null, @@ -69,6 +72,8 @@ export const Context = createContext(initialState); export const Provider = ({ children }: PropsWithChildren) => { const [tabId, setTabId] = useState(null); + const [loading, setLoading] = useState(true); + const loadingTimeout = useRef | null>(null); const [isCurrentTabBeingListenedTo, setIsCurrentTabBeingListenedTo] = useState(false); @@ -137,11 +142,18 @@ export const Provider = ({ children }: PropsWithChildren) => { setTabId(_tabId); const extensionStorage = await chrome.storage.sync.get(); + const _allowedNumberOfTabs = + extensionStorage?.allowedNumberOfTabs || 'single'; - if (extensionStorage?.allowedNumberOfTabs) { - setAllowedNumberOfTabs(extensionStorage?.allowedNumberOfTabs); + if (!extensionStorage?.allowedNumberOfTabs) { + await chrome.storage.sync.clear(); + await chrome.storage.sync.set({ + allowedNumberOfTabs: 'single', + }); } + setAllowedNumberOfTabs(_allowedNumberOfTabs); + if (_tabId) { if (extensionStorage?.allowedNumberOfTabs === 'single') { const getTabBeingListenedTo = await chrome.storage.local.get(); @@ -161,6 +173,7 @@ export const Provider = ({ children }: PropsWithChildren) => { _tabId.toString() !== getTabBeingListenedTo?.tabToRead ) { setIsCurrentTabBeingListenedTo(false); + setLoading(false); return; } else { setIsCurrentTabBeingListenedTo(true); @@ -192,6 +205,7 @@ export const Provider = ({ children }: PropsWithChildren) => { setTabCookies(_cookies); } + setLoading(false); chrome.devtools.inspectedWindow.eval( 'window.location.href', @@ -255,17 +269,22 @@ export const Provider = ({ children }: PropsWithChildren) => { tabId.toString() !== getTabBeingListenedTo?.tabToRead ) { setIsCurrentTabBeingListenedTo(false); + setLoading(false); return; } else { setIsCurrentTabBeingListenedTo(true); - chrome.tabs.query({ active: true }, (tab) => { - if (tab[0]?.url) { - setTabUrl(tab[0]?.url); + chrome.devtools.inspectedWindow.eval( + 'window.location.href', + (result, isException) => { + if (!isException && typeof result === 'string') { + setTabUrl(result); + } } - }); + ); } } } + setLoading(false); }, [tabId, getAllFramesForCurrentTab] ); @@ -296,14 +315,18 @@ export const Provider = ({ children }: PropsWithChildren) => { return Promise.resolve(); }); - chrome.tabs.query({ active: true }, (tab) => { - if (tab[0]?.url) { - setTabUrl(tab[0]?.url); + chrome.devtools.inspectedWindow.eval( + 'window.location.href', + (result, isException) => { + if (!isException && typeof result === 'string') { + setTabUrl(result); + } } - }); + ); await chrome.tabs.reload(Number(changedTabId)); setIsCurrentTabBeingListenedTo(true); + setLoading(false); }, []); const tabUpdateListener = useCallback( @@ -361,6 +384,18 @@ export const Provider = ({ children }: PropsWithChildren) => { changeSyncStorageListener, ]); + useEffect(() => { + loadingTimeout.current = setTimeout(() => { + setLoading(false); + }, 6500); + + return () => { + if (loadingTimeout.current) { + clearTimeout(loadingTimeout.current); + } + }; + }, []); + return ( { tabCookies, tabUrl, tabFrames, + loading, selectedFrame, isCurrentTabBeingListenedTo, returningToSingleTab, diff --git a/packages/extension/src/view/devtools/tests/app.tsx b/packages/extension/src/view/devtools/tests/app.tsx index cbba9617c..fc065ab09 100644 --- a/packages/extension/src/view/devtools/tests/app.tsx +++ b/packages/extension/src/view/devtools/tests/app.tsx @@ -85,6 +85,62 @@ describe('App', () => { expect(await screen.findByTestId('cookies-content')).toBeInTheDocument(); }); + it('should open bounce tracking panel when selected from accordion.', async () => { + await act(() => render()); + // Move to another tab + fireEvent.click(screen.getByTestId('antiCovertTracking-accordion-opener')); + fireEvent.click(screen.getByTestId('Bounce Tracking')); + expect( + await screen.findByTestId('bounce-tracking-content') + ).toBeInTheDocument(); + }); + + it('should open fingerprinting panel when selected from accordion.', async () => { + await act(() => render()); + // Move to another tab + fireEvent.click(screen.getByTestId('antiCovertTracking-accordion-opener')); + fireEvent.click(screen.getByTestId('Fingerprinting')); + expect( + await screen.findByTestId('fingerprinting-content') + ).toBeInTheDocument(); + }); + + it('should open attribution panel when selected from accordion.', async () => { + await act(() => render()); + // Move to another tab + fireEvent.click(screen.getByTestId('privateAdvertising-accordion-opener')); + fireEvent.click(screen.getByTestId('Attribution')); + expect( + await screen.findByTestId('attribution-content') + ).toBeInTheDocument(); + }); + + it('should open topics panel when selected from accordion.', async () => { + await act(() => render()); + // Move to another tab + fireEvent.click(screen.getByTestId('privateAdvertising-accordion-opener')); + fireEvent.click(screen.getByTestId('Topics')); + expect(await screen.findByTestId('topics-content')).toBeInTheDocument(); + }); + + it('should open CHIPS panel when selected from accordion.', async () => { + await act(() => render()); + // Move to another tab + fireEvent.click(screen.getByTestId('siteBoundaries-accordion-opener')); + fireEvent.click(screen.getByTestId('CHIPS')); + expect(await screen.findByTestId('chips-content')).toBeInTheDocument(); + }); + + it('should open Related Website Sets panel when selected from accordion.', async () => { + await act(() => render()); + // Move to another tab + fireEvent.click(screen.getByTestId('siteBoundaries-accordion-opener')); + fireEvent.click(screen.getByTestId('Related Website Sets')); + expect( + await screen.findByTestId('related-website-sets-content') + ).toBeInTheDocument(); + }); + it('should switch to AntiCovert Tracking Panel when clicked', async () => { await act(() => render()); // Click on Bounce Tracking tab diff --git a/packages/extension/src/view/popup/tests/app.tsx b/packages/extension/src/view/popup/tests/app.tsx index 0b2ee1e4d..ace4898f0 100644 --- a/packages/extension/src/view/popup/tests/app.tsx +++ b/packages/extension/src/view/popup/tests/app.tsx @@ -17,8 +17,7 @@ * External dependencies. */ import React from 'react'; -import { render, screen, waitFor } from '@testing-library/react'; -import { act } from 'react-dom/test-utils'; +import { render, screen } from '@testing-library/react'; import '@testing-library/jest-dom'; import SinonChrome from 'sinon-chrome'; @@ -26,165 +25,91 @@ import SinonChrome from 'sinon-chrome'; * Internal dependencies. */ import App from '../app'; -import { Provider as ExternalStoreProvider } from '../stateProviders/syncCookieStore'; +import { useCookieStore } from '../stateProviders/syncCookieStore'; +// @ts-ignore +// eslint-disable-next-line import/no-unresolved +import PSInfo from 'cookie-analysis-tool/data/PSInfo.json'; -const tabCookies = { - _cb: { - parsedCookie: '_cb', - analytics: null, - url: 'https://edition.cnn.com/whatever/api', - headerType: 'response', - frameIdList: [1], - isFirstParty: true, - isCookieSet: true, - }, - pubsyncexp: { - parsedCookie: 'pubsyncexp', - analytics: null, - url: 'https://api.pubmatic.com/whatever/api', - headerType: 'response', - frameIdList: [1], - isFirstParty: false, - isCookieSet: true, - }, - __qca: { - parsedCookie: '__qca', - analytics: { - platform: 'Quantcast', - category: 'Marketing', - name: '__qca', - domain: "Advertiser's website domain", - description: - 'This cookie is set by Quantcast, who present targeted advertising. Stores browser and HTTP request information.', - retention: '1 year', - dataController: 'Quantcast', - gdprUrl: 'https://www.quantcast.com/privacy/', - wildcard: '0', - }, - url: 'https://edition.cnn.com/whatever/api', - headerType: 'response', - frameIdList: [1], - isFirstParty: true, - isCookieSet: true, - }, - KRTBCOOKIE_290: { - parsedCookie: 'KRTBCOOKIE_290', - analytics: { - platform: 'PubMatic', - category: 'Marketing', - name: 'KRTBCOOKIE_*', - domain: 'pubmatic.com', - description: - "Registers a unique ID that identifies the user's device during return visits across websites that use the same ad network. The ID is used to allow targeted ads.", - retention: '29 days', - dataController: 'Pubmatic', - gdprUrl: 'N/A', - wildcard: '1', - }, - url: 'https://api.pubmatic.com/whatever/api', - headerType: 'response', - frameIdList: [1], - isFirstParty: false, - isCookieSet: true, - }, -}; +jest.mock('../stateProviders/syncCookieStore', () => ({ + useCookieStore: jest.fn(), +})); + +const mockUseCookieStore = useCookieStore as jest.Mock; describe('App', () => { - describe('When Tab ID is available', () => { - beforeAll(() => { - globalThis.chrome = SinonChrome as unknown as typeof chrome; - globalThis.chrome = { - ...SinonChrome, - storage: { - //@ts-ignore - local: { - //@ts-ignore - // eslint-disable-next-line @typescript-eslint/no-unused-vars - get: (_, __) => - // eslint-disable-next-line @typescript-eslint/no-explicit-any - new Promise<{ [key: string]: any }>((resolve) => { - resolve({ - 40245632: { cookies: tabCookies }, - tabToRead: '40245632', - }); - }), - //@ts-ignore - onChanged: { - addListener: () => undefined, - removeListener: () => undefined, - }, - }, - sync: { - //@ts-ignore - // eslint-disable-next-line @typescript-eslint/no-unused-vars - get: (_, __) => - // eslint-disable-next-line @typescript-eslint/no-explicit-any - new Promise<{ [key: string]: any }>((resolve) => { - resolve({ - allowedNumberOfTabs: 'single', - }); - }), - //@ts-ignore - onChanged: { - addListener: () => undefined, - removeListener: () => undefined, - }, - }, - }, - devtools: { - //@ts-ignore - inspectedWindow: { - tabId: 40245632, - //@ts-ignore - // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any - eval: (_, callback: any) => { - callback('https://edition.cnn.com'); - }, - }, + beforeAll(() => { + globalThis.chrome = SinonChrome as unknown as typeof chrome; + + globalThis.fetch = function () { + return Promise.resolve({ + json: () => + Promise.resolve({ + ...PSInfo, + }), + }); + } as unknown as typeof fetch; + }); + + it('Should show refresh page message if cookie stats are not available', () => { + mockUseCookieStore.mockReturnValueOnce({ + tabCookieStats: {}, + isCurrentTabBeingListenedTo: true, + }); + render(); + + expect( + screen.getByText('Please try reloading the page') + ).toBeInTheDocument(); + }); + + it('Should show No cookies found on this page message if no firstParty and thirdParty cookies are not available', () => { + mockUseCookieStore.mockReturnValueOnce({ + isCurrentTabBeingListenedTo: true, + cookieStats: { + total: 0, + firstParty: { + total: 0, }, - tabs: { - //@ts-ignore - onUpdated: { - addListener: () => undefined, - removeListener: () => undefined, - }, - //@ts-ignore - // eslint-disable-next-line @typescript-eslint/no-unused-vars - query: (_, __) => { - return [{ id: 40245632, url: 'https://edition.cnn.com' }]; - }, + thirdParty: { + total: 0, }, - }; + }, + initialProcessed: true, + totalProcessed: 100, }); + render(); - it('ExternalStoreProvider should be added to DOM', async () => { - act(() => - render( - - - - ) - ); - expect(await screen.findByText('1st Party Cookies')).toBeInTheDocument(); - expect(await screen.findByText('3rd Party Cookies')).toBeInTheDocument(); - }); + expect( + screen.getByText('No cookies found on this page') + ).toBeInTheDocument(); + }); - it('Initial render will show please refresh page to see cookies', () => { - render( - - - - ); - waitFor( - () => - expect( - screen.getByText('Please refresh this page to view cookies') - ).toBeInTheDocument(), - { - timeout: 11000, - } - ); + it('Should not show No cookies found on this page message if no firstParty and thirdParty cookies are not available', () => { + mockUseCookieStore.mockReturnValueOnce({ + isCurrentTabBeingListenedTo: true, + cookieStats: { + total: 6, + firstParty: { + total: 3, + analytics: 1, + marketing: 1, + functional: 1, + uncategorized: 0, + }, + thirdParty: { + total: 3, + analytics: 1, + marketing: 1, + functional: 1, + uncategorized: 0, + }, + }, }); + render(); + + expect(screen.getAllByText('3').length).toBe(2); + expect(screen.getByText('1st Party Cookies')).toBeInTheDocument(); + expect(screen.getByText('3rd Party Cookies')).toBeInTheDocument(); }); afterAll(() => { diff --git a/packages/extension/src/view/popup/tests/index.tsx b/packages/extension/src/view/popup/tests/index.tsx index ace4898f0..f50b76180 100644 --- a/packages/extension/src/view/popup/tests/index.tsx +++ b/packages/extension/src/view/popup/tests/index.tsx @@ -17,7 +17,7 @@ * External dependencies. */ import React from 'react'; -import { render, screen } from '@testing-library/react'; +import { render, screen, waitFor, act } from '@testing-library/react'; import '@testing-library/jest-dom'; import SinonChrome from 'sinon-chrome'; @@ -25,91 +25,332 @@ import SinonChrome from 'sinon-chrome'; * Internal dependencies. */ import App from '../app'; -import { useCookieStore } from '../stateProviders/syncCookieStore'; -// @ts-ignore -// eslint-disable-next-line import/no-unresolved -import PSInfo from 'cookie-analysis-tool/data/PSInfo.json'; +import { Provider as ExternalStoreProvider } from '../stateProviders/syncCookieStore'; -jest.mock('../stateProviders/syncCookieStore', () => ({ - useCookieStore: jest.fn(), -})); - -const mockUseCookieStore = useCookieStore as jest.Mock; +const tabCookies = { + _cb: { + parsedCookie: '_cb', + analytics: null, + url: 'https://edition.cnn.com/whatever/api', + headerType: 'response', + frameIdList: [1], + isFirstParty: true, + isCookieSet: true, + }, + pubsyncexp: { + parsedCookie: 'pubsyncexp', + analytics: null, + url: 'https://api.pubmatic.com/whatever/api', + headerType: 'response', + frameIdList: [1], + isFirstParty: false, + isCookieSet: true, + }, + __qca: { + parsedCookie: '__qca', + analytics: { + platform: 'Quantcast', + category: 'Marketing', + name: '__qca', + domain: "Advertiser's website domain", + description: + 'This cookie is set by Quantcast, who present targeted advertising. Stores browser and HTTP request information.', + retention: '1 year', + dataController: 'Quantcast', + gdprUrl: 'https://www.quantcast.com/privacy/', + wildcard: '0', + }, + url: 'https://edition.cnn.com/whatever/api', + headerType: 'response', + frameIdList: [1], + isFirstParty: true, + isCookieSet: true, + }, + KRTBCOOKIE_290: { + parsedCookie: 'KRTBCOOKIE_290', + analytics: { + platform: 'PubMatic', + category: 'Marketing', + name: 'KRTBCOOKIE_*', + domain: 'pubmatic.com', + description: + "Registers a unique ID that identifies the user's device during return visits across websites that use the same ad network. The ID is used to allow targeted ads.", + retention: '29 days', + dataController: 'Pubmatic', + gdprUrl: 'N/A', + wildcard: '1', + }, + url: 'https://api.pubmatic.com/whatever/api', + headerType: 'response', + frameIdList: [1], + isFirstParty: false, + isCookieSet: true, + }, +}; describe('App', () => { - beforeAll(() => { - globalThis.chrome = SinonChrome as unknown as typeof chrome; - - globalThis.fetch = function () { - return Promise.resolve({ - json: () => - Promise.resolve({ - ...PSInfo, - }), - }); - } as unknown as typeof fetch; - }); + describe('Single tab is open, but the tabToRead doesnt exists', () => { + beforeAll(() => { + globalThis.chrome = SinonChrome as unknown as typeof chrome; + globalThis.chrome = { + ...SinonChrome, + storage: { + //@ts-ignore + local: { + //@ts-ignore + // eslint-disable-next-line @typescript-eslint/no-unused-vars + get: (_, __) => + // eslint-disable-next-line @typescript-eslint/no-explicit-any + new Promise<{ [key: string]: any }>((resolve) => { + resolve({ + 40245632: { cookies: tabCookies }, + tabToRead: '40245632', + }); + }), + //@ts-ignore + onChanged: { + addListener: () => undefined, + removeListener: () => undefined, + }, + }, + sync: { + //@ts-ignore + // eslint-disable-next-line @typescript-eslint/no-unused-vars + get: (_, __) => + // eslint-disable-next-line @typescript-eslint/no-explicit-any + new Promise<{ [key: string]: any }>((resolve) => { + resolve({ + allowedNumberOfTabs: 'single', + }); + }), + //@ts-ignore + onChanged: { + addListener: () => undefined, + removeListener: () => undefined, + }, + }, + }, + devtools: { + //@ts-ignore + inspectedWindow: { + tabId: 40245632, + //@ts-ignore + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any + eval: (_, callback: any) => { + callback('https://edition.cnn.com'); + }, + }, + }, + tabs: { + //@ts-ignore + onUpdated: { + addListener: () => undefined, + removeListener: () => undefined, + }, + //@ts-ignore + // eslint-disable-next-line @typescript-eslint/no-unused-vars + query: (_, __) => { + return [{ id: 40245632, url: 'https://edition.cnn.com' }]; + }, + }, + }; + }); - it('Should show refresh page message if cookie stats are not available', () => { - mockUseCookieStore.mockReturnValueOnce({ - tabCookieStats: {}, - isCurrentTabBeingListenedTo: true, + it('ExternalStoreProvider should be added to DOM', async () => { + act(() => + render( + + + + ) + ); + expect(await screen.findByText('1st Party Cookies')).toBeInTheDocument(); + expect(await screen.findByText('3rd Party Cookies')).toBeInTheDocument(); }); - render(); - expect( - screen.getByText('Please try reloading the page') - ).toBeInTheDocument(); + it('Initial render will show please refresh page to see cookies', () => { + render( + + + + ); + waitFor( + () => + expect( + screen.getByText('Please refresh this page to view cookies') + ).toBeInTheDocument(), + { + timeout: 11000, + } + ); + }); }); - it('Should show No cookies found on this page message if no firstParty and thirdParty cookies are not available', () => { - mockUseCookieStore.mockReturnValueOnce({ - isCurrentTabBeingListenedTo: true, - cookieStats: { - total: 0, - firstParty: { - total: 0, + describe('Single tab is open, but the tabToRead doesnt exist', () => { + beforeAll(() => { + globalThis.chrome = SinonChrome as unknown as typeof chrome; + globalThis.chrome = { + ...SinonChrome, + storage: { + //@ts-ignore + local: { + //@ts-ignore + // eslint-disable-next-line @typescript-eslint/no-unused-vars + get: (_, __) => + // eslint-disable-next-line @typescript-eslint/no-explicit-any + new Promise<{ [key: string]: any }>((resolve) => { + resolve({ + 40245632: { cookies: tabCookies }, + tabToRead: '4024632', + }); + }), + //@ts-ignore + onChanged: { + addListener: () => undefined, + removeListener: () => undefined, + }, + }, + sync: { + //@ts-ignore + // eslint-disable-next-line @typescript-eslint/no-unused-vars + get: (_, __) => + // eslint-disable-next-line @typescript-eslint/no-explicit-any + new Promise<{ [key: string]: any }>((resolve) => { + resolve({ + allowedNumberOfTabs: 'single', + }); + }), + //@ts-ignore + onChanged: { + addListener: () => undefined, + removeListener: () => undefined, + }, + }, + }, + devtools: { + //@ts-ignore + inspectedWindow: { + tabId: 40245632, + //@ts-ignore + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any + eval: (_, callback: any) => { + callback('https://edition.cnn.com'); + }, + }, }, - thirdParty: { - total: 0, + tabs: { + //@ts-ignore + onUpdated: { + addListener: () => undefined, + removeListener: () => undefined, + }, + //@ts-ignore + // eslint-disable-next-line @typescript-eslint/no-unused-vars + query: (_, __) => { + return [{ id: 40245632, url: 'https://edition.cnn.com' }]; + }, }, - }, - initialProcessed: true, - totalProcessed: 100, + }; }); - render(); - expect( - screen.getByText('No cookies found on this page') - ).toBeInTheDocument(); + it('Popup should display message analyze this tab', async () => { + act(() => + render( + + + + ) + ); + expect(await screen.findByText('Analyze this tab')).toBeInTheDocument(); + expect( + screen.queryByText('This tool works best with a single tab.') + ).not.toBeInTheDocument(); + }); }); - it('Should not show No cookies found on this page message if no firstParty and thirdParty cookies are not available', () => { - mockUseCookieStore.mockReturnValueOnce({ - isCurrentTabBeingListenedTo: true, - cookieStats: { - total: 6, - firstParty: { - total: 3, - analytics: 1, - marketing: 1, - functional: 1, - uncategorized: 0, + describe('Multiple tabs are open, but the tabToRead doesnt exist', () => { + beforeAll(() => { + globalThis.chrome = SinonChrome as unknown as typeof chrome; + globalThis.chrome = { + ...SinonChrome, + storage: { + //@ts-ignore + local: { + //@ts-ignore + // eslint-disable-next-line @typescript-eslint/no-unused-vars + get: (_, __) => + // eslint-disable-next-line @typescript-eslint/no-explicit-any + new Promise<{ [key: string]: any }>((resolve) => { + resolve({ + 40245632: { cookies: tabCookies }, + tabToRead: '4024632', + }); + }), + //@ts-ignore + onChanged: { + addListener: () => undefined, + removeListener: () => undefined, + }, + }, + sync: { + //@ts-ignore + // eslint-disable-next-line @typescript-eslint/no-unused-vars + get: (_, __) => + // eslint-disable-next-line @typescript-eslint/no-explicit-any + new Promise<{ [key: string]: any }>((resolve) => { + resolve({ + allowedNumberOfTabs: 'single', + }); + }), + //@ts-ignore + onChanged: { + addListener: () => undefined, + removeListener: () => undefined, + }, + }, + }, + devtools: { + //@ts-ignore + inspectedWindow: { + tabId: 40245632, + //@ts-ignore + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any + eval: (_, callback: any) => { + callback('https://edition.cnn.com'); + }, + }, }, - thirdParty: { - total: 3, - analytics: 1, - marketing: 1, - functional: 1, - uncategorized: 0, + tabs: { + //@ts-ignore + onUpdated: { + addListener: () => undefined, + removeListener: () => undefined, + }, + //@ts-ignore + // eslint-disable-next-line @typescript-eslint/no-unused-vars + query: (_, __) => { + return [ + { id: 40245632, url: 'https://edition.cnn.com' }, + { id: 4045632, url: 'https://bbc.com' }, + ]; + }, }, - }, + }; }); - render(); - expect(screen.getAllByText('3').length).toBe(2); - expect(screen.getByText('1st Party Cookies')).toBeInTheDocument(); - expect(screen.getByText('3rd Party Cookies')).toBeInTheDocument(); + it('Popup should display message analyze this tab', async () => { + act(() => + render( + + + + ) + ); + expect(await screen.findByText('Analyze this tab')).toBeInTheDocument(); + expect( + await screen.findByText('This tool works best with a single tab.') + ).toBeInTheDocument(); + }); }); afterAll(() => { diff --git a/packages/extension/src/view/settings/app.tsx b/packages/extension/src/view/settings/app.tsx index 8864b279b..9664d35a1 100644 --- a/packages/extension/src/view/settings/app.tsx +++ b/packages/extension/src/view/settings/app.tsx @@ -23,19 +23,12 @@ import React, { useState } from 'react'; * Internal dependencies. */ import './app.css'; -import Settings from './components/tabContent/settings'; +import TABS from './tabs'; import TabHeader from './components/tabHeader'; -const TABS = [ - { - display_name: 'Settings', - Component: Settings, - }, -]; - const App: React.FC = () => { const [selectedTabIndex, setSelectedTabIndex] = useState(0); - const TabContent = TABS[selectedTabIndex].Component; + const TabContent = TABS[selectedTabIndex].component; return (
diff --git a/packages/extension/src/view/settings/components/tabContent/settings/allowedNumberOfTabs/index.tsx b/packages/extension/src/view/settings/components/tabContent/settings/allowedNumberOfTabs/index.tsx index 39ca7c0b0..7e5a6c9f7 100644 --- a/packages/extension/src/view/settings/components/tabContent/settings/allowedNumberOfTabs/index.tsx +++ b/packages/extension/src/view/settings/components/tabContent/settings/allowedNumberOfTabs/index.tsx @@ -62,7 +62,7 @@ const AllowedNumberOfTabs: React.FC = () => { checked={allowedNumberOfTabs === 'unlimited'} /> No restriction (Processing too many tabs may cause the browser to slow - down) + down.) - Tabs would need to be refreshed
); diff --git a/packages/extension/src/view/settings/components/tabHeader/tests/index.tsx b/packages/extension/src/view/settings/components/tabHeader/tests/index.tsx new file mode 100644 index 000000000..027d65b4d --- /dev/null +++ b/packages/extension/src/view/settings/components/tabHeader/tests/index.tsx @@ -0,0 +1,56 @@ +/* + * Copyright 2023 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 { act } from 'react-dom/test-utils'; +import '@testing-library/jest-dom'; + +/** + * Internal dependencies. + */ +import TabHeader from '..'; +import TABS from '../../../tabs'; +describe('TabHeader', () => { + it('Should render without issue.', async () => { + act(() => + render( + tab.display_name)} + selectedTabIndex={0} + setSelectedTabIndex={() => undefined} + /> + ) + ); + expect(await screen.findByText('Settings')).toBeInTheDocument(); + }); + it('Should render without issue.', () => { + const setSelectedIndexMock = jest.fn(); + act(() => + render( + tab.display_name)} + selectedTabIndex={0} + setSelectedTabIndex={setSelectedIndexMock} + /> + ) + ); + fireEvent.click(screen.getByText('Settings')); + expect(setSelectedIndexMock).toHaveBeenCalled(); + }); +}); diff --git a/packages/extension/src/view/settings/tabs.ts b/packages/extension/src/view/settings/tabs.ts new file mode 100644 index 000000000..42c70f6d4 --- /dev/null +++ b/packages/extension/src/view/settings/tabs.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2023 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. + */ +/** + * Internal dependencies. + */ +import Settings from './components/tabContent/settings'; + +const TABS = [ + { + display_name: 'Settings', + id: 'settings', + component: Settings, + }, +]; + +export default TABS; diff --git a/packages/extension/src/view/settings/tests/index.tsx b/packages/extension/src/view/settings/tests/index.tsx new file mode 100644 index 000000000..114fb3090 --- /dev/null +++ b/packages/extension/src/view/settings/tests/index.tsx @@ -0,0 +1,79 @@ +/* + * Copyright 2023 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 { render, screen } from '@testing-library/react'; +import { act } from 'react-dom/test-utils'; +import '@testing-library/jest-dom'; +import SinonChrome from 'sinon-chrome'; + +/** + * Internal dependencies. + */ +import App from '../app'; +import { Provider as SettingsProvider } from '../stateProviders/syncSettingsStore'; +describe('Index', () => { + beforeAll(() => { + globalThis.chrome = SinonChrome as unknown as typeof chrome; + globalThis.chrome = { + ...SinonChrome, + storage: { + //@ts-ignore + local: { + clear: () => Promise.resolve(), + //@ts-ignore + onChanged: { + addListener: () => undefined, + removeListener: () => undefined, + }, + }, + sync: { + //@ts-ignore + // eslint-disable-next-line @typescript-eslint/no-unused-vars + get: (_, __) => + // eslint-disable-next-line @typescript-eslint/no-explicit-any + new Promise<{ [key: string]: any }>((resolve) => { + resolve({ + allowedNumberOfTabs: 'single', + }); + }), + //@ts-ignore + onChanged: { + addListener: () => undefined, + removeListener: () => undefined, + }, + }, + }, + }; + }); + + it('All Providers should be added to DOM', async () => { + act(() => + render( + + + + ) + ); + expect(await screen.findByText('Settings')).toBeInTheDocument(); + }); + + afterAll(() => { + globalThis.chrome = undefined as unknown as typeof chrome; + }); +}); diff --git a/packages/extension/src/worker/service-worker.ts b/packages/extension/src/worker/service-worker.ts index 99a66ec55..5ca3f9323 100644 --- a/packages/extension/src/worker/service-worker.ts +++ b/packages/extension/src/worker/service-worker.ts @@ -286,8 +286,8 @@ chrome.runtime.onInstalled.addListener(async (details) => { }); } if (details.reason === 'update') { - const preSetSettings = chrome.storage.sync.get(); - if (Object.keys(preSetSettings).length === 2) { + const preSetSettings = await chrome.storage.sync.get(); + if (preSetSettings?.allowedNumberOfTabs) { return; } await chrome.storage.sync.clear(); diff --git a/tests/jest.config.cjs b/tests/jest.config.cjs index 382a2bab9..abb740166 100644 --- a/tests/jest.config.cjs +++ b/tests/jest.config.cjs @@ -48,6 +48,7 @@ module.exports = { '/packages/extension/src/view/devtools/index.tsx', '/packages/extension/src/view/popup/index.tsx', '/packages/extension/src/view/devtools/devtools.ts', + '/packages/extension/src/view/settings/index.tsx', ], coverageReporters: ['lcov'], collectCoverageFrom: [