Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: added

Add a Top posts & pages stats widget, wiring the ported presentational widget to live Jetpack Stats data via the designated useStatsTopPosts traffic hook.
1 change: 1 addition & 0 deletions projects/packages/premium-analytics/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"@jetpack-premium-analytics/fields": "link:packages/fields",
"@jetpack-premium-analytics/formatters": "link:packages/formatters",
"@jetpack-premium-analytics/icons": "link:packages/icons",
"@jetpack-premium-analytics/init": "link:packages/init",
"@jetpack-premium-analytics/routing": "link:packages/routing",
"@jetpack-premium-analytics/site-sync": "link:packages/site-sync",
"@jetpack-premium-analytics/ui": "link:packages/ui",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export {
localTZDate,
hasProductFilters,
isSelectablePreset,
computeDateRangeFromPreset,
} from './utils';
export type { ReportDataMap } from './types';
export type { ReportQueryParams } from './api';
Expand Down
4 changes: 4 additions & 0 deletions projects/packages/premium-analytics/tests/jest.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ module.exports = {
rootDir: path.join( __dirname, '..' ),
moduleNameMapper: {
...baseConfig.moduleNameMapper,
// Stub CSS imports (e.g. `@automattic/ui/style.css` pulled in via
// widgets-toolkit, or local `*.module.css`). jest's transformIgnorePatterns
// skips nested node_modules CSS, so it would otherwise be parsed as JS.
'\\.css$': path.join( __dirname, 'style-stub.cjs' ),
// Resolve internal `packages/*` imports to their TypeScript source.
'^@jetpack-premium-analytics/(.*)$': path.join( __dirname, '..', 'packages', '$1', 'src' ),
},
Expand Down
5 changes: 5 additions & 0 deletions projects/packages/premium-analytics/tests/style-stub.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Empty stand-in for CSS imports (e.g. `@automattic/ui/style.css` pulled in via
// widgets-toolkit, or local `*.module.css`). jest's transformIgnorePatterns
// skips nested node_modules CSS, so without this stub it would be parsed as
// JavaScript and throw.
module.exports = {};
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* External dependencies
*/
import { queryClient } from '@jetpack-premium-analytics/data';
import { render, screen } from '@testing-library/react';
import apiFetch from '@wordpress/api-fetch';
/**
* Internal dependencies
*/
import TopPostsWidget from '../render';

jest.mock( '@wordpress/api-fetch', () => jest.fn() );

// WidgetRoot reads URL search params as a fallback for report params; outside
// a matched route the real hook warns and throws.
jest.mock( '@wordpress/route', () => ( {
useSearch: () => ( {} ),
} ) );

const mockApiFetch = apiFetch as unknown as jest.Mock;

// The widget requests a multi-day window, so the stats query layer summarizes
// the views into the top-level `summary` bucket rather than per-day `days`
// buckets.
const TOP_POSTS_RESPONSE = {
date: '2026-06-10',
days: {},
summary: {
postviews: [
{
id: 1,
href: 'https://example.com/hello-world/',
date: '2026-06-01',
title: 'Hello World Post',
type: 'post',
views: 42,
},
{
id: 2,
href: 'https://example.com/about/',
date: null,
title: 'About Page',
type: 'page',
views: 7,
},
],
total_views: 49,
},
};

describe( 'TopPostsWidget', () => {
beforeEach( () => {
// The data package's query client is a module-level singleton; drop its
// cache so each test starts from a fresh fetch.
queryClient.clear();
mockApiFetch.mockReset();
mockApiFetch.mockResolvedValue( TOP_POSTS_RESPONSE );
} );

it( 'renders the fetched top posts as links', async () => {
render( <TopPostsWidget attributes={ { range: 'last-7-days', num: 10 } } /> );

// The `@wordpress/ui` `Link` appends an "(opens in a new tab)" indicator
// to the accessible name, so match the title as a substring.
const link = await screen.findByRole( 'link', { name: /Hello World Post/ } );
expect( link ).toHaveAttribute( 'href', 'https://example.com/hello-world/' );
expect( screen.getByText( 'About Page' ) ).toBeInTheDocument();
} );

it( 'filters rows by post type when the postType attribute is set', async () => {
render( <TopPostsWidget attributes={ { range: 'last-7-days', num: 10, postType: 'page' } } /> );

await expect( screen.findByText( 'About Page' ) ).resolves.toBeInTheDocument();
expect( screen.queryByText( 'Hello World Post' ) ).not.toBeInTheDocument();
} );

it( 'renders the empty state when there are no views', async () => {
mockApiFetch.mockResolvedValue( { date: '2026-06-10', days: {} } );

render( <TopPostsWidget attributes={ { range: 'last-7-days' } } /> );

await expect( screen.findByText( 'No views in this period.' ) ).resolves.toBeInTheDocument();
} );
} );

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "@automattic/jetpack-premium-analytics-widget-top-posts",
"version": "0.1.0-alpha",
"private": true,
"type": "module",
"dependencies": {
"@jetpack-premium-analytics/data": "link:../../packages/data",
"@jetpack-premium-analytics/widgets-toolkit": "link:../../packages/widgets-toolkit",
"@wordpress/i18n": "^6.9.0",
"@wordpress/icons": "^13.0.0",
"@wordpress/ui": "0.13.0",
"@wordpress/widget-primitives": "next",
"date-fns": "4.1.0",
"react": "18.3.1"
},
"devDependencies": {
"@testing-library/react": "16.3.2",
"@wordpress/api-fetch": "7.46.0"
}
}
Loading
Loading