Skip to content

Commit 73d72ac

Browse files
shakyShaneShane Osbourne
andauthored
ntp: privacy stats ordering + translations (#1242)
* added test for rendering order * ntp: privacy stats ordering + translations --------- Co-authored-by: Shane Osbourne <[email protected]>
1 parent 3d9cd9f commit 73d72ac

File tree

9 files changed

+138
-21
lines changed

9 files changed

+138
-21
lines changed

special-pages/messages/new-tab/examples/stats.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
const privacyStatsData = {
55
totalCount: 12345,
66
trackerCompanies: [
7+
{ displayName: 'Tracker Co. C', count: 91011 },
78
{ displayName: 'Tracker Co. A', count: 1234 },
9+
{ displayName: '__other__', count: 89901 },
810
{ displayName: 'Tracker Co. B', count: 5678 },
9-
{ displayName: 'Tracker Co. C', count: 91011 },
1011
],
1112
};
1213

special-pages/pages/new-tab/app/privacy-stats/PrivacyStats.js

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { useVisibility } from '../widget-list/widget-config.provider.js';
77
import { viewTransition } from '../utils.js';
88
import { ShowHideButton } from '../components/ShowHideButton.jsx';
99
import { useCustomizer } from '../customizer/Customizer.js';
10+
import { DDG_STATS_OTHER_COMPANY_IDENTIFIER } from './constants.js';
11+
import { sortStatsForDisplay } from './privacy-stats.utils.js';
1012

1113
/**
1214
* @typedef {import('../../../../types/new-tab').TrackerCompany} TrackerCompany
@@ -135,23 +137,29 @@ export function Heading({ expansion, trackerCompanies, totalCount, onToggle, but
135137
// eslint-disable-next-line no-redeclare
136138
export function Body({ trackerCompanies, listAttrs = {} }) {
137139
const max = trackerCompanies[0]?.count ?? 0;
140+
const { t } = useTypedTranslation();
138141
const [formatter] = useState(() => new Intl.NumberFormat());
142+
const sorted = sortStatsForDisplay(trackerCompanies);
139143

140144
return (
141-
<ul {...listAttrs} class={styles.list}>
142-
{trackerCompanies.map((company) => {
145+
<ul {...listAttrs} class={styles.list} data-testid="CompanyList">
146+
{sorted.map((company) => {
143147
const percentage = Math.min((company.count * 100) / max, 100);
144148
const valueOrMin = Math.max(percentage, 10);
145149
const inlineStyles = {
146150
width: `${valueOrMin}%`,
147151
};
148152
const countText = formatter.format(company.count);
153+
// prettier-ignore
154+
const displayName = company.displayName === DDG_STATS_OTHER_COMPANY_IDENTIFIER
155+
? t('trackerStatsOtherCompanyName')
156+
: company.displayName;
149157
return (
150158
<li key={company.displayName}>
151159
<div class={styles.row}>
152160
<div class={styles.company}>
153-
<CompanyIcon company={company} />
154-
<span class={styles.name}>{company.displayName}</span>
161+
<CompanyIcon displayName={company.displayName} />
162+
<span class={styles.name}>{displayName}</span>
155163
</div>
156164
<span class={styles.count}>{countText}</span>
157165
<span class={styles.bar}></span>
@@ -210,15 +218,19 @@ export function PrivacyStatsConsumer() {
210218
return null;
211219
}
212220

213-
function CompanyIcon({ company }) {
214-
const icon = company.displayName.toLowerCase().split('.')[0];
221+
/**
222+
* @param {object} props
223+
* @param {string} props.displayName
224+
*/
225+
function CompanyIcon({ displayName }) {
226+
const icon = displayName.toLowerCase().split('.')[0];
215227
const cleaned = icon.replace(/ /g, '-');
216228
const firstChar = icon[0];
217229

218230
return (
219231
<span className={styles.icon}>
220-
{icon === 'other' && <Other />}
221-
{icon !== 'other' && (
232+
{icon === DDG_STATS_OTHER_COMPANY_IDENTIFIER && <Other />}
233+
{icon !== DDG_STATS_OTHER_COMPANY_IDENTIFIER && (
222234
<img
223235
src={`./company-icons/${cleaned}.svg`}
224236
alt={icon + ' icon'}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/**
2+
* @module Privacy Stats Constants
3+
*/
4+
export const DDG_STATS_OTHER_COMPANY_IDENTIFIER = '__other__';

special-pages/pages/new-tab/app/privacy-stats/integration-tests/privacy-stats.spec.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,13 @@ test.describe('newtab privacy stats', () => {
1414
expect(calls1.length).toBe(1);
1515
expect(calls2.length).toBe(1);
1616
expect(calls3.length).toBe(1);
17+
18+
const listItems = page.getByTestId('CompanyList').locator('li');
19+
expect(await listItems.count()).toBe(5);
20+
expect(await listItems.nth(0).textContent()).toBe('Facebook310');
21+
expect(await listItems.nth(1).textContent()).toBe('Google279');
22+
expect(await listItems.nth(2).textContent()).toBe('Amazon67');
23+
expect(await listItems.nth(3).textContent()).toBe('Google Ads2');
24+
expect(await listItems.nth(4).textContent()).toBe('Other210');
1725
});
1826
});
Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
// todo: add schema for types here.
1+
import { DDG_STATS_OTHER_COMPANY_IDENTIFIER } from '../constants.js';
2+
3+
/**
4+
* @import { PrivacyStatsData } from "../../../../../types/new-tab";
5+
* @type {Record<string, PrivacyStatsData>}
6+
*/
27
export const stats = {
38
few: {
49
totalCount: 481_113,
@@ -11,6 +16,10 @@ export const stats = {
1116
displayName: 'Google',
1217
count: 279,
1318
},
19+
{
20+
displayName: DDG_STATS_OTHER_COMPANY_IDENTIFIER,
21+
count: 210,
22+
},
1423
{
1524
displayName: 'Amazon',
1625
count: 67,
@@ -19,12 +28,7 @@ export const stats = {
1928
displayName: 'Google Ads',
2029
count: 2,
2130
},
22-
{
23-
displayName: 'Other',
24-
count: 210,
25-
},
2631
],
27-
trackerCompaniesPeriod: 'last-day',
2832
},
2933
single: {
3034
totalCount: 481_113,
@@ -34,16 +38,13 @@ export const stats = {
3438
count: 1,
3539
},
3640
],
37-
trackerCompaniesPeriod: 'last-day',
3841
},
3942
norecent: {
4043
totalCount: 481_113,
4144
trackerCompanies: [],
42-
trackerCompaniesPeriod: 'last-day',
4345
},
4446
none: {
4547
totalCount: 0,
4648
trackerCompanies: [],
47-
trackerCompaniesPeriod: 'last-day',
4849
},
4950
};

special-pages/pages/new-tab/app/privacy-stats/privacy-stats.md

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,27 @@ title: Privacy Stats
3030
}
3131
```
3232

33-
## Examples:
34-
The following examples show the data types in JSON format:
33+
## Example:
34+
35+
Note: The frontend will re-order the list based on the following two rules:
36+
37+
* First, descending order, from the highest count to lowest
38+
* Second, the special entry `__other__` will always be placed at the end.
39+
40+
So, the following input is fine, no need for the native side to put the list into any order
41+
42+
```json
43+
{
44+
"totalCount": 12345,
45+
"trackerCompanies": [
46+
{ "displayName": "__other__", "count": 89901 },
47+
{ "displayName": "Tracker Co. C", "count": 91011 },
48+
{ "displayName": "Tracker Co. A", "count": 1234 },
49+
{ "displayName": "Tracker Co. B", "count": 5678 }
50+
]
51+
}
52+
```
53+
54+
The following examples show the data types in JSON format (these are type-checked, so can be replied upon)
3555
[messages/new-tab/examples/stats.js](../../../../messages/new-tab/examples/stats.js)
56+
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { DDG_STATS_OTHER_COMPANY_IDENTIFIER } from './constants.js';
2+
3+
/**
4+
* Sort into descending order + place __other__ at the end.
5+
*
6+
* @import { TrackerCompany } from "../../../../types/new-tab"
7+
* @param {TrackerCompany[]} stats
8+
* @return {TrackerCompany[]}
9+
*/
10+
export function sortStatsForDisplay(stats) {
11+
const sorted = stats.sort((a, b) => b.count - a.count);
12+
const other = sorted.findIndex((x) => x.displayName === DDG_STATS_OTHER_COMPANY_IDENTIFIER);
13+
if (other > -1) {
14+
const popped = sorted.splice(other, 1);
15+
sorted.push(popped[0]);
16+
}
17+
return sorted;
18+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { describe, it } from 'node:test';
2+
import { deepEqual } from 'node:assert/strict';
3+
import { stats } from '../mocks/stats.js';
4+
import { sortStatsForDisplay } from '../privacy-stats.utils.js';
5+
6+
/**
7+
* @import { TrackerCompany } from "../../../../../types/new-tab"
8+
*/
9+
10+
describe('stats re-ordering', () => {
11+
it('orders based on count + places __other__ at the end of the list', () => {
12+
/** @type {TrackerCompany[]} */
13+
const input = stats.few.trackerCompanies;
14+
const expected = [
15+
{ displayName: 'Facebook', count: 310 },
16+
{ displayName: 'Google', count: 279 },
17+
{ displayName: 'Amazon', count: 67 },
18+
{ displayName: 'Google Ads', count: 2 },
19+
{ displayName: '__other__', count: 210 },
20+
];
21+
const actual = sortStatsForDisplay(input);
22+
deepEqual(actual, expected);
23+
});
24+
it('orders when other is absent', () => {
25+
/** @type {TrackerCompany[]} */
26+
const input = [
27+
{ displayName: 'Google', count: 279 },
28+
{ displayName: 'Google Ads', count: 2 },
29+
{ displayName: 'Amazon', count: 67 },
30+
{ displayName: 'Facebook', count: 310 },
31+
];
32+
const expected = [
33+
{ displayName: 'Facebook', count: 310 },
34+
{ displayName: 'Google', count: 279 },
35+
{ displayName: 'Amazon', count: 67 },
36+
{ displayName: 'Google Ads', count: 2 },
37+
];
38+
const actual = sortStatsForDisplay(input);
39+
deepEqual(actual, expected);
40+
});
41+
it('sorts a single item', () => {
42+
/** @type {TrackerCompany[]} */
43+
const input = [{ displayName: 'Google', count: 279 }];
44+
const expected = [{ displayName: 'Google', count: 279 }];
45+
const actual = sortStatsForDisplay(input);
46+
deepEqual(actual, expected);
47+
});
48+
});

special-pages/pages/new-tab/src/locales/en/newtab.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@
6161
"title": "Hide recent activity",
6262
"note": "The aria-label text for a toggle button that hides the detailed activity feed"
6363
},
64+
"trackerStatsOtherCompanyName": {
65+
"title": "Other",
66+
"note": "A placeholder to represent an aggregated count of entries, not present in the rest of the list. For example, 'Other: 200', which would mean 200 entries excluding the ones already shown"
67+
},
6468
"favorites_show_less": {
6569
"title": "Show less",
6670
"note": ""
@@ -181,4 +185,4 @@
181185
"title": "Pinned to Taskbar!",
182186
"note": "Button text after clicking on the Next Steps card for adding DDG app to OS dock"
183187
}
184-
}
188+
}

0 commit comments

Comments
 (0)