Skip to content

Commit 7923678

Browse files
shakyShaneShane Osbourne
andauthored
ntp: display tweaks + stats show more fix (#1288)
* ntp: display tweaks * added a failing test case * fixed the implementation * More robust tests --------- Co-authored-by: Shane Osbourne <[email protected]>
1 parent f5ad800 commit 7923678

File tree

9 files changed

+211
-49
lines changed

9 files changed

+211
-49
lines changed

messaging/lib/messaging.types.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ interface Window {
2020
__playwright_01: {
2121
mockResponses: Record<string, import('../index.js').MessageResponse>;
2222
subscriptionEvents: import('../index.js').SubscriptionEvent[];
23+
publishSubscriptionEvent?: (evt: import('../index.js').SubscriptionEvent) => void;
2324
mocks: {
2425
outgoing: UnstableMockCall[];
2526
};

messaging/lib/test-utils.mjs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,14 @@ export function simulateSubscriptionMessage(params) {
427427
window[params.name](subscriptionEvent);
428428
break;
429429
}
430+
case 'integration': {
431+
if (!('publishSubscriptionEvent' in window.__playwright_01))
432+
throw new Error(
433+
`subscription event '${subscriptionEvent.subscriptionName}' was not published because 'window.__playwright_01.publishSubscriptionEvent' was missing`,
434+
);
435+
window.__playwright_01.publishSubscriptionEvent?.(subscriptionEvent);
436+
break;
437+
}
430438
default:
431439
throw new Error('platform not supported yet: ' + params.injectName);
432440
}

special-pages/pages/new-tab/app/entry-points/privacyStats.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Centered } from '../components/Layout.js';
44

55
export function factory() {
66
return (
7-
<Centered>
7+
<Centered data-entry-point="privacyStats">
88
<PrivacyStatsCustomized />
99
</Centered>
1010
);

special-pages/pages/new-tab/app/mock-transport.js

Lines changed: 57 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,23 @@ import { variants as nextSteps } from './next-steps/nextsteps.data.js';
1616
* @typedef {import('../../../types/new-tab').NextStepsData} NextStepsData
1717
* @typedef {import('../../../types/new-tab').UpdateNotificationData} UpdateNotificationData
1818
* @typedef {import('../../../types/new-tab').NewTabMessages['subscriptions']['subscriptionEvent']} SubscriptionNames
19+
* @typedef {import('@duckduckgo/messaging/lib/test-utils.mjs').SubscriptionEvent} SubscriptionEvent
1920
*/
2021

2122
const VERSION_PREFIX = '__ntp_29__.';
2223
const url = new URL(window.location.href);
2324

2425
export function mockTransport() {
2526
const channel = new BroadcastChannel('ntp');
27+
/** @type {Map<string, (d: any)=>void>} */
28+
const subscriptions = new Map();
29+
if ('__playwright_01' in window) {
30+
window.__playwright_01.publishSubscriptionEvent = (/** @type {SubscriptionEvent} */ evt) => {
31+
const matchingCallback = subscriptions.get(evt.subscriptionName);
32+
if (!matchingCallback) return console.error('no matching callback for subscription', evt);
33+
matchingCallback(evt.params);
34+
};
35+
}
2636

2737
function broadcast(named) {
2838
setTimeout(() => {
@@ -153,9 +163,17 @@ export function mockTransport() {
153163
}
154164
},
155165
subscribe(_msg, cb) {
156-
window.__playwright_01?.mocks?.outgoing?.push?.({ payload: structuredClone(_msg) });
157166
/** @type {import('../../../types/new-tab.ts').NewTabMessages['subscriptions']['subscriptionEvent']} */
158167
const sub = /** @type {any} */ (_msg.subscriptionName);
168+
169+
if ('__playwright_01' in window) {
170+
window.__playwright_01?.mocks?.outgoing?.push?.({ payload: structuredClone(_msg) });
171+
subscriptions.set(sub, cb);
172+
return () => {
173+
subscriptions.delete(sub);
174+
};
175+
}
176+
159177
switch (sub) {
160178
case 'widgets_onConfigUpdated': {
161179
const controller = new AbortController();
@@ -256,30 +274,46 @@ export function mockTransport() {
256274
}
257275
case 'stats_onDataUpdate': {
258276
const statsVariant = url.searchParams.get('stats');
259-
if (statsVariant !== 'willUpdate') return () => {};
260-
261277
const count = url.searchParams.get('stats-update-count');
262-
const max = Math.min(parseInt(count || '0'), 10);
263-
if (max === 0) return () => {};
264-
265-
let inc = 1;
266-
const int = setInterval(() => {
267-
if (inc === max) return clearInterval(int);
268-
const next = {
269-
...stats.willUpdate,
270-
trackerCompanies: stats.willUpdate.trackerCompanies.map((x, index) => {
271-
return {
272-
...x,
273-
count: x.count + inc * index,
274-
};
275-
}),
278+
const updateMaxCount = parseInt(count || '0');
279+
if (updateMaxCount === 0) return () => {};
280+
if (statsVariant === 'willUpdate') {
281+
let inc = 1;
282+
const max = Math.min(updateMaxCount, 10);
283+
const int = setInterval(() => {
284+
if (inc === max) return clearInterval(int);
285+
const next = {
286+
...stats.willUpdate,
287+
trackerCompanies: stats.willUpdate.trackerCompanies.map((x, index) => {
288+
return {
289+
...x,
290+
count: x.count + inc * index,
291+
};
292+
}),
293+
};
294+
cb(next);
295+
inc++;
296+
}, 500);
297+
return () => {
298+
clearInterval(int);
276299
};
277-
cb(next);
278-
inc++;
279-
}, 500);
280-
return () => {
281-
clearInterval(int);
282-
};
300+
} else if (statsVariant === 'growing') {
301+
const list = stats.many.trackerCompanies;
302+
let index = 0;
303+
const max = Math.min(updateMaxCount, list.length);
304+
const int = setInterval(() => {
305+
if (index === max) return clearInterval(int);
306+
console.log({ index, max });
307+
cb({
308+
trackerCompanies: list.slice(0, index + 1),
309+
});
310+
index++;
311+
}, 200);
312+
return () => {};
313+
} else {
314+
console.log(statsVariant);
315+
return () => {};
316+
}
283317
}
284318
case 'favorites_onConfigUpdate': {
285319
const controller = new AbortController();

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

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { viewTransition } from '../../utils.js';
99
import { ShowHideButton } from '../../components/ShowHideButton.jsx';
1010
import { useCustomizer } from '../../customizer/components/Customizer.js';
1111
import { DDG_STATS_OTHER_COMPANY_IDENTIFIER } from '../constants.js';
12-
import { sortStatsForDisplay } from '../privacy-stats.utils.js';
12+
import { displayNameForCompany, sortStatsForDisplay } from '../privacy-stats.utils.js';
1313

1414
/**
1515
* @import enStrings from "../strings.json"
@@ -138,35 +138,30 @@ export function PrivacyStatsBody({ trackerCompanies, listAttrs = {} }) {
138138
const defaultRowMax = 5;
139139
const sorted = sortStatsForDisplay(trackerCompanies);
140140
const max = sorted[0]?.count ?? 0;
141-
const [visible, setVisible] = useState(defaultRowMax);
142-
const hasmore = sorted.length > visible;
141+
const [expansion, setExpansion] = useState(/** @type {Expansion} */ ('collapsed'));
143142

144143
const toggleListExpansion = () => {
145-
if (hasmore) {
144+
if (expansion === 'collapsed') {
146145
messaging.statsShowMore();
147146
} else {
148147
messaging.statsShowLess();
149148
}
150-
if (visible === defaultRowMax) {
151-
setVisible(sorted.length);
152-
}
153-
if (visible === sorted.length) {
154-
setVisible(defaultRowMax);
155-
}
149+
setExpansion(expansion === 'collapsed' ? 'expanded' : 'collapsed');
156150
};
157151

152+
const rows = expansion === 'expanded' ? sorted : sorted.slice(0, defaultRowMax);
153+
158154
return (
159155
<Fragment>
160156
<ul {...listAttrs} class={styles.list} data-testid="CompanyList">
161-
{sorted.slice(0, visible).map((company) => {
157+
{rows.map((company) => {
162158
const percentage = Math.min((company.count * 100) / max, 100);
163159
const valueOrMin = Math.max(percentage, 10);
164160
const inlineStyles = {
165161
width: `${valueOrMin}%`,
166162
};
167163
const countText = formatter.format(company.count);
168-
// prettier-ignore
169-
const displayName = company.displayName
164+
const displayName = displayNameForCompany(company.displayName);
170165
if (company.displayName === DDG_STATS_OTHER_COMPANY_IDENTIFIER) {
171166
const otherText = t('stats_otherCount', { count: String(company.count) });
172167
return (
@@ -178,7 +173,7 @@ export function PrivacyStatsBody({ trackerCompanies, listAttrs = {} }) {
178173
return (
179174
<li key={company.displayName} class={styles.row}>
180175
<div class={styles.company}>
181-
<CompanyIcon displayName={company.displayName} />
176+
<CompanyIcon displayName={displayName} />
182177
<span class={styles.name}>{displayName}</span>
183178
</div>
184179
<span class={styles.count}>{countText}</span>
@@ -192,11 +187,11 @@ export function PrivacyStatsBody({ trackerCompanies, listAttrs = {} }) {
192187
<div class={styles.listExpander}>
193188
<ShowHideButton
194189
onClick={toggleListExpansion}
195-
text={hasmore ? t('ntp_show_more') : t('ntp_show_less')}
190+
text={expansion === 'collapsed' ? t('ntp_show_more') : t('ntp_show_less')}
196191
showText={true}
197192
buttonAttrs={{
198-
'aria-expanded': !hasmore,
199-
'aria-pressed': visible === sorted.length,
193+
'aria-expanded': expansion === 'expanded',
194+
'aria-pressed': expansion === 'expanded',
200195
}}
201196
/>
202197
</div>
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { stats } from '../mocks/stats.js';
2+
import { expect } from '@playwright/test';
3+
4+
export class PrivacyStatsPage {
5+
/**
6+
* @param {import("@playwright/test").Page} page
7+
* @param {import("../../../integration-tests/new-tab.page.js").NewtabPage} ntp
8+
*/
9+
constructor(page, ntp) {
10+
this.page = page;
11+
this.ntp = ntp;
12+
}
13+
14+
/**
15+
* @param {object} params
16+
* @param {number} params.count
17+
*/
18+
async receive({ count }) {
19+
/** @type {import("../../../../../types/new-tab.js").PrivacyStatsData} */
20+
const next = { totalCount: 0, trackerCompanies: stats.many.trackerCompanies.slice(0, count) };
21+
await this.ntp.mocks.simulateSubscriptionMessage('stats_onDataUpdate', next);
22+
}
23+
24+
/**
25+
* @param {import("../../../../../types/new-tab.js").PrivacyStatsData} data
26+
*/
27+
async receiveData(data) {
28+
await this.ntp.mocks.simulateSubscriptionMessage('stats_onDataUpdate', data);
29+
}
30+
31+
context() {
32+
return this.page.locator('[data-entry-point="privacyStats"]');
33+
}
34+
35+
rows() {
36+
return this.context().getByTestId('CompanyList').locator('li');
37+
}
38+
39+
/**
40+
* @param {number} count
41+
*/
42+
async hasRows(count) {
43+
const rows = this.rows();
44+
expect(await rows.count()).toBe(count);
45+
}
46+
47+
async showMoreSecondary() {
48+
await this.context().getByLabel('Show More', { exact: true }).click();
49+
}
50+
51+
async showLessSecondary() {
52+
await this.context().getByLabel('Show Less', { exact: true }).click();
53+
}
54+
}

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

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { test, expect } from '@playwright/test';
1+
import { expect, test } from '@playwright/test';
22
import { NewtabPage } from '../../../integration-tests/new-tab.page.js';
3+
import { PrivacyStatsPage } from './privacy-stats.page.js';
34

45
test.describe('newtab privacy stats', () => {
56
test('fetches config + stats', async ({ page }, workerInfo) => {
@@ -29,12 +30,19 @@ test.describe('newtab privacy stats', () => {
2930
});
3031
test('sending a pixel when show more is clicked', async ({ page }, workerInfo) => {
3132
const ntp = NewtabPage.create(page, workerInfo);
33+
const psp = new PrivacyStatsPage(page, ntp);
3234
await ntp.reducedMotion();
3335
await ntp.openPage({ additional: { stats: 'many' } });
34-
await page.getByLabel('Show More', { exact: true }).click();
35-
await page.getByLabel('Show Less').click();
36+
37+
// show + hide
38+
await psp.showMoreSecondary();
39+
await psp.showLessSecondary();
40+
41+
// assert the event were sent
3642
await ntp.mocks.waitForCallCount({ method: 'stats_showMore', count: 1 });
3743
await ntp.mocks.waitForCallCount({ method: 'stats_showLess', count: 1 });
44+
45+
// to re-instate later
3846
// expect(calls1.length).toBe(2);
3947
// expect(calls1).toStrictEqual([
4048
// {
@@ -82,13 +90,59 @@ test.describe('newtab privacy stats', () => {
8290
},
8391
async ({ page }, workerInfo) => {
8492
const ntp = NewtabPage.create(page, workerInfo);
93+
const psp = new PrivacyStatsPage(page, ntp);
8594
await ntp.reducedMotion();
86-
await ntp.openPage({ additional: { stats: 'willUpdate', 'stats-update-count': '2' } });
95+
await ntp.openPage({ additional: { stats: 'none' } });
96+
97+
await psp.receiveData({
98+
totalCount: 2,
99+
trackerCompanies: [
100+
{ displayName: 'Google', count: 1 },
101+
{ displayName: 'Facebook', count: 1 },
102+
],
103+
});
104+
105+
await page.getByText('Google1').locator('[style="width: 100%;"]').waitFor();
106+
await page.getByText('Facebook1').locator('[style="width: 100%;"]').waitFor();
107+
108+
await psp.receiveData({
109+
totalCount: 2,
110+
trackerCompanies: [
111+
{ displayName: 'Google', count: 5 },
112+
{ displayName: 'Facebook', count: 1 },
113+
],
114+
});
87115

88-
//
89116
// Checking the first + last bar widths due to a regression
90-
await page.getByText('Google Ads5').locator('[style="width: 100%;"]').waitFor();
117+
await page.getByText('Google5').locator('[style="width: 100%;"]').waitFor();
91118
await page.getByText('Facebook1').locator('[style="width: 20%;"]').waitFor();
92119
},
93120
);
121+
test(
122+
'secondary expansion',
123+
{
124+
annotation: {
125+
type: 'issue',
126+
description: 'https://app.asana.com/0/1201141132935289/1208861172991227/f',
127+
},
128+
},
129+
async ({ page }, workerInfo) => {
130+
const ntp = NewtabPage.create(page, workerInfo);
131+
const psp = new PrivacyStatsPage(page, ntp);
132+
await ntp.reducedMotion();
133+
await ntp.openPage({ additional: { stats: 'none' } });
134+
135+
// deliver enough companies to show the 'show more' toggle
136+
await psp.receive({ count: 6 });
137+
await psp.hasRows(5); // 1 is hidden
138+
await psp.showMoreSecondary();
139+
140+
await psp.receive({ count: 7 });
141+
await psp.hasRows(7);
142+
await psp.showLessSecondary();
143+
144+
await psp.receive({ count: 2 });
145+
await psp.hasRows(2);
146+
},
147+
);
94148
});

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export const stats = {
2121
count: 210,
2222
},
2323
{
24-
displayName: 'Amazon',
24+
displayName: 'Amazon.com',
2525
count: 67,
2626
},
2727
{
@@ -63,7 +63,7 @@ export const stats = {
6363
count: 1,
6464
},
6565
{
66-
displayName: 'Amazon',
66+
displayName: 'Amazon.com',
6767
count: 1,
6868
},
6969
{
@@ -72,6 +72,10 @@ export const stats = {
7272
},
7373
],
7474
},
75+
growing: {
76+
totalCount: 0,
77+
trackerCompanies: [],
78+
},
7579
many: {
7680
totalCount: 890,
7781
trackerCompanies: [

0 commit comments

Comments
 (0)