Skip to content

Commit 6c9cda7

Browse files
sadpandajoeclaude
andauthored
chore: update chart list e2e and component tests (#34393)
Co-authored-by: Claude <[email protected]>
1 parent 967134f commit 6c9cda7

File tree

12 files changed

+3531
-505
lines changed

12 files changed

+3531
-505
lines changed

superset-frontend/cypress-base/cypress/e2e/chart_list/list.test.ts

Lines changed: 0 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -94,67 +94,12 @@ describe('Charts list', () => {
9494
});
9595
});
9696

97-
describe('list mode', () => {
98-
before(() => {
99-
visitChartList();
100-
setGridMode('list');
101-
});
102-
103-
it('should load rows in list mode', () => {
104-
cy.getBySel('listview-table').should('be.visible');
105-
cy.getBySel('sort-header').eq(1).contains('Name');
106-
cy.getBySel('sort-header').eq(2).contains('Type');
107-
cy.getBySel('sort-header').eq(3).contains('Dataset');
108-
cy.getBySel('sort-header').eq(4).contains('On dashboards');
109-
cy.getBySel('sort-header').eq(5).contains('Owners');
110-
cy.getBySel('sort-header').eq(6).contains('Last modified');
111-
cy.getBySel('sort-header').eq(7).contains('Actions');
112-
});
113-
114-
it('should bulk select in list mode', () => {
115-
toggleBulkSelect();
116-
cy.get('[aria-label="Select all"]').click();
117-
cy.get('input[type="checkbox"]:checked').should('have.length', 26);
118-
cy.getBySel('bulk-select-copy').contains('25 Selected');
119-
cy.getBySel('bulk-select-action')
120-
.should('have.length', 2)
121-
.then($btns => {
122-
expect($btns).to.contain('Delete');
123-
expect($btns).to.contain('Export');
124-
});
125-
cy.getBySel('bulk-select-deselect-all').click();
126-
cy.get('input[type="checkbox"]:checked').should('have.length', 0);
127-
cy.getBySel('bulk-select-copy').contains('0 Selected');
128-
cy.getBySel('bulk-select-action').should('not.exist');
129-
});
130-
});
131-
13297
describe('card mode', () => {
13398
before(() => {
13499
visitChartList();
135100
setGridMode('card');
136101
});
137102

138-
it('should load rows in card mode', () => {
139-
cy.getBySel('listview-table').should('not.exist');
140-
cy.getBySel('styled-card').should('have.length', 25);
141-
});
142-
143-
it('should bulk select in card mode', () => {
144-
toggleBulkSelect();
145-
cy.getBySel('styled-card').click({ multiple: true });
146-
cy.getBySel('bulk-select-copy').contains('25 Selected');
147-
cy.getBySel('bulk-select-action')
148-
.should('have.length', 2)
149-
.then($btns => {
150-
expect($btns).to.contain('Delete');
151-
expect($btns).to.contain('Export');
152-
});
153-
cy.getBySel('bulk-select-deselect-all').click();
154-
cy.getBySel('bulk-select-copy').contains('0 Selected');
155-
cy.getBySel('bulk-select-action').should('not.exist');
156-
});
157-
158103
it('should preserve other filters when sorting', () => {
159104
cy.getBySel('styled-card').should('have.length', 25);
160105
setFilter('Type', 'Big Number');

superset-frontend/src/components/FacePile/FacePile.test.tsx

Lines changed: 96 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,29 @@
1616
* specific language governing permissions and limitations
1717
* under the License.
1818
*/
19-
import { act, fireEvent, render, screen } from 'spec/helpers/testing-library';
19+
import {
20+
act,
21+
fireEvent,
22+
render,
23+
screen,
24+
within,
25+
cleanup,
26+
} from 'spec/helpers/testing-library';
2027
import { store } from 'src/views/store';
28+
import { isFeatureEnabled } from '@superset-ui/core';
2129
import { FacePile } from '.';
2230
import { getRandomColor } from './utils';
2331

32+
// Mock the feature flag
33+
jest.mock('@superset-ui/core', () => ({
34+
...jest.requireActual('@superset-ui/core'),
35+
isFeatureEnabled: jest.fn(),
36+
}));
37+
38+
const mockIsFeatureEnabled = isFeatureEnabled as jest.MockedFunction<
39+
typeof isFeatureEnabled
40+
>;
41+
2442
const users = [...new Array(10)].map((_, i) => ({
2543
first_name: 'user',
2644
last_name: `${i}`,
@@ -29,37 +47,99 @@ const users = [...new Array(10)].map((_, i) => ({
2947

3048
beforeEach(() => {
3149
jest.useFakeTimers();
50+
// Default to Slack avatars disabled
51+
mockIsFeatureEnabled.mockImplementation(() => false);
3252
});
3353

3454
afterEach(() => {
3555
jest.useRealTimers();
56+
mockIsFeatureEnabled.mockReset();
57+
cleanup();
3658
});
3759

3860
describe('FacePile', () => {
39-
let container: HTMLElement;
61+
it('renders empty state with no users', () => {
62+
const { container } = render(<FacePile users={[]} />, { store });
63+
64+
expect(container.querySelector('.ant-avatar-group')).toBeInTheDocument();
65+
expect(container.querySelectorAll('.ant-avatar')).toHaveLength(0);
66+
});
67+
68+
it('renders single user without truncation', () => {
69+
const { container } = render(<FacePile users={users.slice(0, 1)} />, {
70+
store,
71+
});
72+
73+
const avatars = container.querySelectorAll('.ant-avatar');
74+
expect(avatars).toHaveLength(1);
75+
expect(within(container).getByText('U0')).toBeInTheDocument();
76+
expect(within(container).queryByText(/\+/)).not.toBeInTheDocument();
77+
});
78+
79+
it('renders multiple users no truncation', () => {
80+
const { container } = render(<FacePile users={users.slice(0, 4)} />, {
81+
store,
82+
});
4083

41-
beforeEach(() => {
42-
({ container } = render(<FacePile users={users} />, { store }));
84+
const avatars = container.querySelectorAll('.ant-avatar');
85+
expect(avatars).toHaveLength(4);
86+
expect(within(container).getByText('U0')).toBeInTheDocument();
87+
expect(within(container).getByText('U1')).toBeInTheDocument();
88+
expect(within(container).getByText('U2')).toBeInTheDocument();
89+
expect(within(container).getByText('U3')).toBeInTheDocument();
90+
expect(within(container).queryByText(/\+/)).not.toBeInTheDocument();
4391
});
4492

45-
it('is a valid element', () => {
46-
const exposedFaces = screen.getAllByText(/U/);
47-
expect(exposedFaces).toHaveLength(4);
48-
const overflownFaces = screen.getByText('+6');
49-
expect(overflownFaces).toBeVisible();
93+
it('renders multiple users with truncation', () => {
94+
const { container } = render(<FacePile users={users} />, { store });
5095

51-
// Display user info when hovering over one of exposed face in the pile.
52-
fireEvent.mouseEnter(exposedFaces[0]);
96+
// Should show 4 avatars + 1 overflow indicator = 5 total elements
97+
const avatars = container.querySelectorAll('.ant-avatar');
98+
expect(avatars).toHaveLength(5);
99+
100+
// Should show first 4 users
101+
expect(within(container).getByText('U0')).toBeInTheDocument();
102+
expect(within(container).getByText('U1')).toBeInTheDocument();
103+
expect(within(container).getByText('U2')).toBeInTheDocument();
104+
expect(within(container).getByText('U3')).toBeInTheDocument();
105+
106+
// Should show overflow count (+6 because 10 total - 4 shown)
107+
expect(within(container).getByText('+6')).toBeInTheDocument();
108+
});
109+
110+
it('displays user tooltip on hover', () => {
111+
const { container } = render(<FacePile users={users.slice(0, 2)} />, {
112+
store,
113+
});
114+
115+
const firstAvatar = within(container).getByText('U0');
116+
fireEvent.mouseEnter(firstAvatar);
53117
act(() => jest.runAllTimers());
118+
54119
expect(screen.getByRole('tooltip')).toHaveTextContent('user 0');
55120
});
56121

57-
it('renders an Avatar', () => {
58-
expect(container.querySelector('.ant-avatar')).toBeVisible();
59-
});
122+
it('displays avatar images when Slack avatars are enabled', () => {
123+
// Enable Slack avatars feature flag
124+
mockIsFeatureEnabled.mockImplementation(
125+
feature => feature === 'SLACK_ENABLE_AVATARS',
126+
);
127+
128+
const { container: testContainer } = render(
129+
<FacePile users={users.slice(0, 2)} />,
130+
{
131+
store,
132+
},
133+
);
134+
135+
const avatars = testContainer.querySelectorAll('.ant-avatar');
136+
expect(avatars).toHaveLength(2);
60137

61-
it('hides overflow', () => {
62-
expect(container.querySelectorAll('.ant-avatar')).toHaveLength(5);
138+
// Should have img elements with correct src attributes
139+
const imgs = testContainer.querySelectorAll('.ant-avatar img');
140+
expect(imgs).toHaveLength(2);
141+
expect(imgs[0]).toHaveAttribute('src', '/api/v1/user/0/avatar.png');
142+
expect(imgs[1]).toHaveAttribute('src', '/api/v1/user/1/avatar.png');
63143
});
64144
});
65145

superset-frontend/src/components/Tag/utils.test.tsx

Lines changed: 166 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
* specific language governing permissions and limitations
1717
* under the License.
1818
*/
19-
import { tagToSelectOption } from 'src/components/Tag/utils';
19+
import fetchMock from 'fetch-mock';
20+
import rison from 'rison';
21+
import { tagToSelectOption, loadTags } from 'src/components/Tag/utils';
2022

2123
describe('tagToSelectOption', () => {
2224
test('converts a Tag object with table_name to a SelectTagsValue', () => {
@@ -35,3 +37,166 @@ describe('tagToSelectOption', () => {
3537
expect(tagToSelectOption(tag)).toEqual(expectedSelectTagsValue);
3638
});
3739
});
40+
41+
describe('loadTags', () => {
42+
beforeEach(() => {
43+
fetchMock.reset();
44+
});
45+
46+
afterEach(() => {
47+
fetchMock.restore();
48+
});
49+
50+
test('constructs correct API query with custom tag filter', async () => {
51+
const mockTags = [
52+
{ id: 1, name: 'analytics', type: 1 },
53+
{ id: 2, name: 'finance', type: 1 },
54+
];
55+
56+
fetchMock.get('glob:*/api/v1/tag/*', {
57+
result: mockTags,
58+
count: 2,
59+
});
60+
61+
await loadTags('analytics', 0, 25);
62+
63+
// Verify the API was called with correct parameters
64+
const calls = fetchMock.calls();
65+
expect(calls).toHaveLength(1);
66+
67+
const [url] = calls[0];
68+
expect(url).toContain('/api/v1/tag/?q=');
69+
70+
// Extract and decode the query parameter
71+
const urlObj = new URL(url);
72+
const queryParam = urlObj.searchParams.get('q');
73+
expect(queryParam).not.toBeNull();
74+
const decodedQuery = rison.decode(queryParam!) as Record<string, any>;
75+
76+
// Verify the query structure
77+
expect(decodedQuery).toEqual({
78+
filters: [
79+
{ col: 'name', opr: 'ct', value: 'analytics' },
80+
{ col: 'type', opr: 'custom_tag', value: true },
81+
],
82+
page: 0,
83+
page_size: 25,
84+
order_column: 'name',
85+
order_direction: 'asc',
86+
});
87+
});
88+
89+
test('returns correctly transformed data', async () => {
90+
const mockTags = [
91+
{ id: 1, name: 'analytics', type: 1 },
92+
{ id: 2, name: 'finance', type: 1 },
93+
];
94+
95+
fetchMock.get('glob:*/api/v1/tag/*', {
96+
result: mockTags,
97+
count: 2,
98+
});
99+
100+
const result = await loadTags('', 0, 25);
101+
102+
expect(result).toEqual({
103+
data: [
104+
{ value: 1, label: 'analytics', key: 1 },
105+
{ value: 2, label: 'finance', key: 2 },
106+
],
107+
totalCount: 2,
108+
});
109+
});
110+
111+
test('handles search parameter correctly', async () => {
112+
fetchMock.get('glob:*/api/v1/tag/*', {
113+
result: [],
114+
count: 0,
115+
});
116+
117+
await loadTags('financial-data', 0, 25);
118+
119+
const calls = fetchMock.calls();
120+
const [url] = calls[0];
121+
const urlObj = new URL(url);
122+
const queryParam = urlObj.searchParams.get('q');
123+
expect(queryParam).not.toBeNull();
124+
const decodedQuery = rison.decode(queryParam!) as Record<string, any>;
125+
126+
// Should include the search term in the name filter
127+
expect(decodedQuery.filters[0]).toEqual({
128+
col: 'name',
129+
opr: 'ct',
130+
value: 'financial-data',
131+
});
132+
});
133+
134+
test('handles pagination parameters correctly', async () => {
135+
fetchMock.get('glob:*/api/v1/tag/*', {
136+
result: [],
137+
count: 0,
138+
});
139+
140+
await loadTags('', 2, 10);
141+
142+
const calls = fetchMock.calls();
143+
const [url] = calls[0];
144+
const urlObj = new URL(url);
145+
const queryParam = urlObj.searchParams.get('q');
146+
expect(queryParam).not.toBeNull();
147+
const decodedQuery = rison.decode(queryParam!) as Record<string, any>;
148+
149+
expect(decodedQuery.page).toBe(2);
150+
expect(decodedQuery.page_size).toBe(10);
151+
});
152+
153+
test('always includes custom tag filter regardless of other parameters', async () => {
154+
fetchMock.get('glob:*/api/v1/tag/*', {
155+
result: [],
156+
count: 0,
157+
});
158+
159+
// Test with different combinations of parameters
160+
await loadTags('', 0, 25);
161+
await loadTags('search-term', 1, 50);
162+
await loadTags('another-search', 5, 100);
163+
164+
const calls = fetchMock.calls();
165+
166+
// Verify all calls include the custom tag filter
167+
calls.forEach(call => {
168+
const [url] = call;
169+
const urlObj = new URL(url);
170+
const queryParam = urlObj.searchParams.get('q');
171+
expect(queryParam).not.toBeNull();
172+
const decodedQuery = rison.decode(queryParam!) as Record<string, any>;
173+
174+
// Every call should have the custom tag filter
175+
expect(decodedQuery.filters).toContainEqual({
176+
col: 'type',
177+
opr: 'custom_tag',
178+
value: true,
179+
});
180+
});
181+
});
182+
183+
test('maintains correct order specification', async () => {
184+
fetchMock.get('glob:*/api/v1/tag/*', {
185+
result: [],
186+
count: 0,
187+
});
188+
189+
await loadTags('test', 0, 25);
190+
191+
const calls = fetchMock.calls();
192+
const [url] = calls[0];
193+
const urlObj = new URL(url);
194+
const queryParam = urlObj.searchParams.get('q');
195+
expect(queryParam).not.toBeNull();
196+
const decodedQuery = rison.decode(queryParam!) as Record<string, any>;
197+
198+
// Should always order by name ascending
199+
expect(decodedQuery.order_column).toBe('name');
200+
expect(decodedQuery.order_direction).toBe('asc');
201+
});
202+
});

0 commit comments

Comments
 (0)