Skip to content

Commit 60bc17c

Browse files
Merge pull request #12888 from google/enhancement/12803-partial-data-full-breakdown
Enhancement/12803 partial data full breakdown
2 parents 75b2bf9 + 134f0ad commit 60bc17c

40 files changed

Lines changed: 4959 additions & 232 deletions
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/**
2+
* Site Goals BreakdownTabs tests.
3+
*
4+
* Site Kit by Google, Copyright 2026 Google LLC
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* https://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
/**
20+
* Internal dependencies
21+
*/
22+
import { SITE_GOALS_BREAKDOWN_OTHER_SOURCES_TAB_ID } from '@/js/modules/analytics-4/components/site-goals/constants';
23+
import { fireEvent, render } from '@tests/js/test-utils';
24+
import BreakdownTabs from './BreakdownTabs';
25+
26+
describe( 'BreakdownTabs', () => {
27+
const tabs = [
28+
{ id: 'woocommerce', label: 'WooCommerce' },
29+
{ id: 'easy-digital-downloads', label: 'Easy Digital Downloads' },
30+
];
31+
32+
it( 'renders the provided tabs plus an "Other sources" tab', () => {
33+
const { getByText } = render(
34+
<BreakdownTabs
35+
tabs={ tabs }
36+
activeTabID="woocommerce"
37+
onTabChange={ () => {} }
38+
/>
39+
);
40+
41+
expect( getByText( 'WooCommerce' ) ).toBeInTheDocument();
42+
expect( getByText( 'Easy Digital Downloads' ) ).toBeInTheDocument();
43+
expect( getByText( 'Other sources' ) ).toBeInTheDocument();
44+
} );
45+
46+
it( 'fires onTabChange with the selected tab ID', () => {
47+
const onTabChange = jest.fn();
48+
49+
const { getByText } = render(
50+
<BreakdownTabs
51+
tabs={ tabs }
52+
activeTabID="woocommerce"
53+
onTabChange={ onTabChange }
54+
/>
55+
);
56+
57+
fireEvent.click( getByText( 'Easy Digital Downloads' ) );
58+
expect( onTabChange ).toHaveBeenCalledWith( 'easy-digital-downloads' );
59+
60+
fireEvent.click( getByText( 'Other sources' ) );
61+
expect( onTabChange ).toHaveBeenCalledWith(
62+
SITE_GOALS_BREAKDOWN_OTHER_SOURCES_TAB_ID
63+
);
64+
} );
65+
66+
it( 'renders an info tooltip for tabs that provide one', () => {
67+
const { container } = render(
68+
<BreakdownTabs
69+
tabs={ [
70+
{
71+
id: 'woocommerce',
72+
label: 'WooCommerce',
73+
tooltip: 'This form was created with WPForms.',
74+
},
75+
] }
76+
activeTabID="woocommerce"
77+
onTabChange={ () => {} }
78+
/>
79+
);
80+
81+
// Tabs with tooltip content render the info icon; "Other sources" does not.
82+
expect(
83+
container.querySelectorAll( '.googlesitekit-info-tooltip' )
84+
).toHaveLength( 1 );
85+
} );
86+
87+
it( 'renders every tab when many are present', () => {
88+
const manyTabs = Array.from( { length: 12 }, ( _, index ) => ( {
89+
id: `form-${ index }`,
90+
label: `Form ${ index }`,
91+
} ) );
92+
93+
const { getByText } = render(
94+
<BreakdownTabs
95+
tabs={ manyTabs }
96+
activeTabID="form-0"
97+
onTabChange={ () => {} }
98+
/>
99+
);
100+
101+
manyTabs.forEach( ( tab ) => {
102+
expect( getByText( tab.label ) ).toBeInTheDocument();
103+
} );
104+
expect( getByText( 'Other sources' ) ).toBeInTheDocument();
105+
} );
106+
} );
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/**
2+
* Site Goals breakdown tabs.
3+
*
4+
* Site Kit by Google, Copyright 2026 Google LLC
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* https://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
/**
20+
* External dependencies
21+
*/
22+
import { FC, ReactNode } from 'react';
23+
24+
/**
25+
* WordPress dependencies
26+
*/
27+
import { __ } from '@wordpress/i18n';
28+
29+
/**
30+
* Internal dependencies
31+
*/
32+
import { Tab, TabBar } from 'googlesitekit-components';
33+
import InfoTooltip from '@/js/components/InfoTooltip';
34+
import { SITE_GOALS_BREAKDOWN_OTHER_SOURCES_TAB_ID } from '@/js/modules/analytics-4/components/site-goals/constants';
35+
36+
export interface BreakdownTab {
37+
id: string;
38+
label: string;
39+
tooltip?: ReactNode;
40+
}
41+
42+
interface BreakdownTabsProps {
43+
tabs: BreakdownTab[];
44+
activeTabID: string;
45+
onTabChange: ( tabID: string ) => void;
46+
showOtherSources?: boolean;
47+
otherSourcesLabel?: string;
48+
}
49+
50+
const BreakdownTabs: FC< BreakdownTabsProps > = ( {
51+
tabs,
52+
activeTabID,
53+
onTabChange,
54+
showOtherSources = true,
55+
otherSourcesLabel,
56+
} ) => {
57+
// The trailing "Other sources" tab aggregates every event without a value
58+
// for the breakdown dimension; only appended when such events exist.
59+
const allTabs: BreakdownTab[] = [
60+
...tabs,
61+
...( showOtherSources
62+
? [
63+
{
64+
id: SITE_GOALS_BREAKDOWN_OTHER_SOURCES_TAB_ID,
65+
label:
66+
otherSourcesLabel ||
67+
__( 'Other sources', 'google-site-kit' ),
68+
},
69+
]
70+
: [] ),
71+
];
72+
73+
const activeIndex = allTabs.findIndex( ( tab ) => tab.id === activeTabID );
74+
75+
return (
76+
<div className="googlesitekit-site-goals-breakdown-tabs">
77+
<TabBar
78+
activeIndex={ activeIndex < 0 ? 0 : activeIndex }
79+
handleActiveIndexUpdate={ ( index: number ) => {
80+
const tab = allTabs[ index ];
81+
82+
if ( tab ) {
83+
onTabChange( tab.id );
84+
}
85+
} }
86+
>
87+
{ allTabs.map( ( tab ) => (
88+
<Tab
89+
key={ tab.id }
90+
className="mdc-tab--min-width"
91+
focusOnActivate={ false }
92+
>
93+
<span className="mdc-tab__text-label">
94+
{ tab.label }
95+
</span>
96+
{ tab.tooltip && <InfoTooltip title={ tab.tooltip } /> }
97+
</Tab>
98+
) ) }
99+
</TabBar>
100+
</div>
101+
);
102+
};
103+
104+
export default BreakdownTabs;
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
* Key action tiles tests.
3+
*
4+
* Site Kit by Google, Copyright 2026 Google LLC
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* https://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
/**
20+
* Internal dependencies
21+
*/
22+
import { render } from '@tests/js/test-utils';
23+
import KeyActionTiles from './KeyActionTiles';
24+
25+
describe( 'KeyActionTiles', () => {
26+
const props = {
27+
supportURL: 'https://example.com/help',
28+
rateTitle: 'Sales Rate',
29+
totalTitle: 'Total Sales',
30+
totalSubtitle: '“purchase” events',
31+
currentRate: 0.5,
32+
previousRate: 0.4,
33+
currentSessions: 100,
34+
currentCount: 42,
35+
previousCount: 30,
36+
otherSourcesCount: 7,
37+
otherSourcesPreviousCount: 3,
38+
};
39+
40+
it( 'renders the rate and total tiles, using the value-tab count, on a value tab', () => {
41+
const { getByText, queryByText } = render(
42+
<KeyActionTiles { ...props } isOtherSourcesTab={ false } />
43+
);
44+
45+
expect( getByText( 'Sales Rate' ) ).toBeInTheDocument();
46+
expect( getByText( 'Total Sales' ) ).toBeInTheDocument();
47+
// The value-tab count is shown, not the Other sources count.
48+
expect( getByText( '42' ) ).toBeInTheDocument();
49+
expect( queryByText( '7' ) ).not.toBeInTheDocument();
50+
} );
51+
52+
it( 'omits the rate tile and uses the unattributed count on the Other sources tab', () => {
53+
const { getByText, queryByText } = render(
54+
<KeyActionTiles { ...props } isOtherSourcesTab />
55+
);
56+
57+
// No rate tile (no per-source sessions to rate against).
58+
expect( queryByText( 'Sales Rate' ) ).not.toBeInTheDocument();
59+
expect( getByText( 'Total Sales' ) ).toBeInTheDocument();
60+
// The unattributed count is shown instead of the value-tab count.
61+
expect( getByText( '7' ) ).toBeInTheDocument();
62+
expect( queryByText( '42' ) ).not.toBeInTheDocument();
63+
} );
64+
} );

0 commit comments

Comments
 (0)