gabotorresruiz commented on code in PR #43805:
URL: https://github.com/apache/superset/pull/43805#discussion_r4085368710
##########
docs/docs/using-superset/exporting-dashboard-data.mdx:
##########
@@ -9,16 +9,18 @@ version: 1
Superset can export every chart on a dashboard to a single Excel workbook, with
each chart's underlying data rendered as its own worksheet. The export reflects
-the dashboard's currently applied filters and runs asynchronously: when it
-finishes, the requesting user receives an email with a time-limited download
-link.
+the dashboard's currently applied filters and runs asynchronously: the page
+polls for completion and downloads the workbook automatically, and a logged-in
+user with an email address also receives a time-limited download link by email.
+Sessions with no email on file — embedded guest-token sessions and anonymous
Review Comment:
Good catch. The note now says skipped charts are listed in an `Export
Summary` worksheet added to the workbook, with the email mentioned as the
secondary channel when one is sent.
##########
superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadMenuItems.test.tsx:
##########
@@ -368,3 +533,258 @@ test('Enabled screenshot items should not show tooltip
icon', () => {
mockIsFeatureEnabled.mockReset();
});
+
+// ---------------------------------------------------------------------------
+// Delivery follows the requester identity, not iframe presence: a guest or
+// anonymous session (no userId) has no email channel, so the toast must not
+// promise one, and the image export (webdriver-rendered, guests cannot open
+// Explore) is hidden.
+// ---------------------------------------------------------------------------
+
+const guestState = { user: {} };
+
+test('guest session: export toast promises auto-download, not an email', async
() => {
+ mockSupersetClient.post.mockResolvedValue({
+ json: { job_id: 'abc' },
+ } as never);
+
+ render(<MenuWrapper />, { useRedux: true, initialState: guestState });
+
+ await userEvent.click(screen.getByText('Export Data to Excel'));
+
+ await waitFor(() =>
+ expect(mockAddInfoToast).toHaveBeenCalledWith(
+ 'Your export is being generated. Please, do not leave the page.',
+ { noDuplicate: true },
+ ),
+ );
+});
+
+test('guest session: Export Images to Excel is hidden even with the webdriver
enabled', () => {
+ enableWebDriverScreenshot();
+
+ render(<MenuWrapper />, { useRedux: true, initialState: guestState });
+
+ expect(screen.getByText('Export Data to Excel')).toBeInTheDocument();
+ expect(screen.queryByText('Export Images to Excel')).not.toBeInTheDocument();
+});
+
+test('logged-in user without an email gets the delivery-neutral toast', async
() => {
+ mockSupersetClient.post.mockResolvedValue({
+ json: { job_id: 'abc' },
+ } as never);
+
+ render(<MenuWrapper />, {
+ useRedux: true,
+ initialState: { user: { userId: 1 } },
+ });
+
+ await userEvent.click(screen.getByText('Export Data to Excel'));
+
+ await waitFor(() =>
+ expect(mockAddInfoToast).toHaveBeenCalledWith(
+ 'Your export is being generated. Please, do not leave the page.',
+ { noDuplicate: true },
+ ),
+ );
+});
+
+test('a "running" status restarts the wait window, so queue delay is not
counted', async () => {
+ jest.useFakeTimers();
+ mockSupersetClient.post.mockResolvedValue({
+ json: { job_id: 'abc' },
+ } as never);
+ // Worker picks the job up on the first poll; still running just past the
+ // original 12 minute deadline; done on the poll after that.
+ mockSupersetClient.get
+ .mockResolvedValueOnce({ json: { status: 'running' } } as never)
+ .mockResolvedValueOnce({ json: { status: 'running' } } as never)
+ .mockResolvedValueOnce({
+ json: {
+ status: 'ready',
+ download_url: '/api/v1/dashboard/export_xlsx/download/abc/',
+ },
+ } as never);
+
+ render(<MenuWrapper />, { useRedux: true, initialState: loggedInState });
+
+ await userEvent.click(screen.getByText('Export Data to Excel'));
+ await waitFor(() => expect(mockSupersetClient.post).toHaveBeenCalled());
+
+ await act(async () => {
+ jest.advanceTimersByTime(3000);
+ });
+ await waitFor(() => expect(mockSupersetClient.get).toHaveBeenCalledTimes(1));
+
+ // t ~= 12m01s: past the enqueue-based deadline, within the restarted one
+ // (running was observed at t=3s). Without the restart this poll would give
+ // up with a danger toast instead of continuing.
+ await act(async () => {
+ jest.advanceTimersByTime(12 * 60 * 1000 - 2000);
+ });
+ await waitFor(() => expect(mockSupersetClient.get).toHaveBeenCalledTimes(2));
+ expect(mockAddDangerToast).not.toHaveBeenCalled();
+
+ await act(async () => {
+ jest.advanceTimersByTime(3000);
+ });
+ await waitFor(() => {
+
expect(lastIframeSrc()).toBe('/api/v1/dashboard/export_xlsx/download/abc/');
+ expect(mockAddSuccessToast).toHaveBeenCalledWith(
+ 'Your export is ready and downloading.',
+ );
+ });
+});
+
+test('a transient poll failure keeps polling and still downloads', async () =>
{
+ jest.useFakeTimers();
+ mockSupersetClient.post.mockResolvedValue({
+ json: { job_id: 'abc' },
+ } as never);
+ mockSupersetClient.get
+ .mockRejectedValueOnce(new Error('network blip'))
+ .mockResolvedValueOnce({
+ json: {
+ status: 'ready',
+ download_url: '/api/v1/dashboard/export_xlsx/download/abc/',
+ },
+ } as never);
+
+ render(<MenuWrapper />, { useRedux: true, initialState: loggedInState });
+
+ await userEvent.click(screen.getByText('Export Data to Excel'));
+ await waitFor(() => expect(mockSupersetClient.post).toHaveBeenCalled());
+
+ await act(async () => {
+ jest.advanceTimersByTime(3000);
+ });
+ await waitFor(() => expect(mockSupersetClient.get).toHaveBeenCalledTimes(1));
+ expect(mockAddDangerToast).not.toHaveBeenCalled();
+
+ await act(async () => {
+ jest.advanceTimersByTime(3000);
+ });
+ await waitFor(() => {
+
expect(lastIframeSrc()).toBe('/api/v1/dashboard/export_xlsx/download/abc/');
+ expect(mockAddSuccessToast).toHaveBeenCalledWith(
+ 'Your export is ready and downloading.',
+ );
+ });
+});
+
+test('poll failures past the deadline give up with an error toast', async ()
=> {
+ jest.useFakeTimers();
+ mockSupersetClient.post.mockResolvedValue({
+ json: { job_id: 'abc' },
+ } as never);
+ mockSupersetClient.get.mockRejectedValue(new Error('server down'));
+
+ render(<MenuWrapper />, { useRedux: true, initialState: loggedInState });
+
+ await userEvent.click(screen.getByText('Export Data to Excel'));
+ await waitFor(() => expect(mockSupersetClient.post).toHaveBeenCalled());
+
+ await act(async () => {
+ jest.advanceTimersByTime(3000);
+ });
+ await waitFor(() => expect(mockSupersetClient.get).toHaveBeenCalledTimes(1));
+
+ // Jump past the 12 minute deadline; the next failing poll must give up.
+ await act(async () => {
+ jest.advanceTimersByTime(13 * 60 * 1000);
+ });
+ await waitFor(() => {
+ expect(mockAddDangerToast).toHaveBeenCalledWith(
+ 'Sorry, something went wrong. Try again later.',
+ );
+ });
+ expect(mockAddSuccessToast).not.toHaveBeenCalledWith(
+ 'Your export is ready and downloading.',
+ );
+});
+
+test('a "ready" status with no download_url is an error, not a fake success',
async () => {
+ jest.useFakeTimers();
+ mockSupersetClient.post.mockResolvedValue({
+ json: { job_id: 'abc' },
+ } as never);
+ mockSupersetClient.get.mockResolvedValue({
+ json: { status: 'ready' },
+ } as never);
+
+ render(<MenuWrapper />, { useRedux: true, initialState: loggedInState });
+
+ await userEvent.click(screen.getByText('Export Data to Excel'));
+ await waitFor(() => expect(mockSupersetClient.post).toHaveBeenCalled());
+
+ await act(async () => {
+ jest.advanceTimersByTime(3000);
+ });
+
+ await waitFor(() => {
+ expect(mockAddDangerToast).toHaveBeenCalledWith(
+ 'Sorry, something went wrong. Try again later.',
+ );
+ });
+ expect(mockAddSuccessToast).not.toHaveBeenCalled();
+ expect(mockAddSuccessToast).not.toHaveBeenCalledWith(
+ 'Your export is ready and downloading.',
+ );
+});
+
+test('unmounting stops the polling loop', async () => {
+ jest.useFakeTimers();
+ mockSupersetClient.post.mockResolvedValue({
+ json: { job_id: 'abc' },
+ } as never);
+ mockSupersetClient.get.mockResolvedValue({
+ json: { status: 'pending' },
+ } as never);
+
+ const { unmount } = render(<MenuWrapper />, {
+ useRedux: true,
+ initialState: loggedInState,
+ });
+
+ await userEvent.click(screen.getByText('Export Data to Excel'));
+ await waitFor(() => expect(mockSupersetClient.post).toHaveBeenCalled());
+
+ await act(async () => {
+ jest.advanceTimersByTime(3000);
+ });
+ await waitFor(() => expect(mockSupersetClient.get).toHaveBeenCalledTimes(1));
+
+ unmount();
+
+ await act(async () => {
+ jest.advanceTimersByTime(30000);
+ });
+ expect(mockSupersetClient.get).toHaveBeenCalledTimes(1);
+});
+
+test('the pending toast is announced once, not re-emitted on every poll',
async () => {
+ jest.useFakeTimers();
Review Comment:
This was written against user-event v12 internals; the repo is on v14, and
these tests now click through the `clickMenuItem` helper with `delay: null`, so
nothing waits on a timer under fake timers. The suite passes in CI.
##########
superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadMenuItems.test.tsx:
##########
@@ -368,3 +533,258 @@ test('Enabled screenshot items should not show tooltip
icon', () => {
mockIsFeatureEnabled.mockReset();
});
+
+// ---------------------------------------------------------------------------
+// Delivery follows the requester identity, not iframe presence: a guest or
+// anonymous session (no userId) has no email channel, so the toast must not
+// promise one, and the image export (webdriver-rendered, guests cannot open
+// Explore) is hidden.
+// ---------------------------------------------------------------------------
+
+const guestState = { user: {} };
+
+test('guest session: export toast promises auto-download, not an email', async
() => {
+ mockSupersetClient.post.mockResolvedValue({
+ json: { job_id: 'abc' },
+ } as never);
+
+ render(<MenuWrapper />, { useRedux: true, initialState: guestState });
+
+ await userEvent.click(screen.getByText('Export Data to Excel'));
+
+ await waitFor(() =>
+ expect(mockAddInfoToast).toHaveBeenCalledWith(
+ 'Your export is being generated. Please, do not leave the page.',
+ { noDuplicate: true },
+ ),
+ );
+});
+
+test('guest session: Export Images to Excel is hidden even with the webdriver
enabled', () => {
+ enableWebDriverScreenshot();
+
+ render(<MenuWrapper />, { useRedux: true, initialState: guestState });
+
+ expect(screen.getByText('Export Data to Excel')).toBeInTheDocument();
+ expect(screen.queryByText('Export Images to Excel')).not.toBeInTheDocument();
+});
+
+test('logged-in user without an email gets the delivery-neutral toast', async
() => {
+ mockSupersetClient.post.mockResolvedValue({
+ json: { job_id: 'abc' },
+ } as never);
+
+ render(<MenuWrapper />, {
+ useRedux: true,
+ initialState: { user: { userId: 1 } },
+ });
+
+ await userEvent.click(screen.getByText('Export Data to Excel'));
+
+ await waitFor(() =>
+ expect(mockAddInfoToast).toHaveBeenCalledWith(
+ 'Your export is being generated. Please, do not leave the page.',
+ { noDuplicate: true },
+ ),
+ );
+});
+
+test('a "running" status restarts the wait window, so queue delay is not
counted', async () => {
+ jest.useFakeTimers();
+ mockSupersetClient.post.mockResolvedValue({
+ json: { job_id: 'abc' },
+ } as never);
+ // Worker picks the job up on the first poll; still running just past the
+ // original 12 minute deadline; done on the poll after that.
+ mockSupersetClient.get
+ .mockResolvedValueOnce({ json: { status: 'running' } } as never)
+ .mockResolvedValueOnce({ json: { status: 'running' } } as never)
+ .mockResolvedValueOnce({
+ json: {
+ status: 'ready',
+ download_url: '/api/v1/dashboard/export_xlsx/download/abc/',
+ },
+ } as never);
+
+ render(<MenuWrapper />, { useRedux: true, initialState: loggedInState });
+
+ await userEvent.click(screen.getByText('Export Data to Excel'));
+ await waitFor(() => expect(mockSupersetClient.post).toHaveBeenCalled());
+
+ await act(async () => {
+ jest.advanceTimersByTime(3000);
+ });
+ await waitFor(() => expect(mockSupersetClient.get).toHaveBeenCalledTimes(1));
+
+ // t ~= 12m01s: past the enqueue-based deadline, within the restarted one
+ // (running was observed at t=3s). Without the restart this poll would give
+ // up with a danger toast instead of continuing.
+ await act(async () => {
+ jest.advanceTimersByTime(12 * 60 * 1000 - 2000);
+ });
+ await waitFor(() => expect(mockSupersetClient.get).toHaveBeenCalledTimes(2));
+ expect(mockAddDangerToast).not.toHaveBeenCalled();
+
+ await act(async () => {
+ jest.advanceTimersByTime(3000);
+ });
+ await waitFor(() => {
+
expect(lastIframeSrc()).toBe('/api/v1/dashboard/export_xlsx/download/abc/');
+ expect(mockAddSuccessToast).toHaveBeenCalledWith(
+ 'Your export is ready and downloading.',
+ );
+ });
+});
+
+test('a transient poll failure keeps polling and still downloads', async () =>
{
+ jest.useFakeTimers();
+ mockSupersetClient.post.mockResolvedValue({
+ json: { job_id: 'abc' },
+ } as never);
+ mockSupersetClient.get
+ .mockRejectedValueOnce(new Error('network blip'))
+ .mockResolvedValueOnce({
+ json: {
+ status: 'ready',
+ download_url: '/api/v1/dashboard/export_xlsx/download/abc/',
+ },
+ } as never);
+
+ render(<MenuWrapper />, { useRedux: true, initialState: loggedInState });
+
+ await userEvent.click(screen.getByText('Export Data to Excel'));
+ await waitFor(() => expect(mockSupersetClient.post).toHaveBeenCalled());
+
+ await act(async () => {
+ jest.advanceTimersByTime(3000);
+ });
+ await waitFor(() => expect(mockSupersetClient.get).toHaveBeenCalledTimes(1));
+ expect(mockAddDangerToast).not.toHaveBeenCalled();
+
+ await act(async () => {
+ jest.advanceTimersByTime(3000);
+ });
+ await waitFor(() => {
+
expect(lastIframeSrc()).toBe('/api/v1/dashboard/export_xlsx/download/abc/');
+ expect(mockAddSuccessToast).toHaveBeenCalledWith(
+ 'Your export is ready and downloading.',
+ );
+ });
+});
+
+test('poll failures past the deadline give up with an error toast', async ()
=> {
+ jest.useFakeTimers();
+ mockSupersetClient.post.mockResolvedValue({
+ json: { job_id: 'abc' },
+ } as never);
+ mockSupersetClient.get.mockRejectedValue(new Error('server down'));
+
+ render(<MenuWrapper />, { useRedux: true, initialState: loggedInState });
+
+ await userEvent.click(screen.getByText('Export Data to Excel'));
+ await waitFor(() => expect(mockSupersetClient.post).toHaveBeenCalled());
+
+ await act(async () => {
+ jest.advanceTimersByTime(3000);
+ });
+ await waitFor(() => expect(mockSupersetClient.get).toHaveBeenCalledTimes(1));
+
+ // Jump past the 12 minute deadline; the next failing poll must give up.
+ await act(async () => {
+ jest.advanceTimersByTime(13 * 60 * 1000);
+ });
+ await waitFor(() => {
+ expect(mockAddDangerToast).toHaveBeenCalledWith(
+ 'Sorry, something went wrong. Try again later.',
+ );
+ });
+ expect(mockAddSuccessToast).not.toHaveBeenCalledWith(
+ 'Your export is ready and downloading.',
+ );
+});
+
+test('a "ready" status with no download_url is an error, not a fake success',
async () => {
+ jest.useFakeTimers();
+ mockSupersetClient.post.mockResolvedValue({
+ json: { job_id: 'abc' },
+ } as never);
+ mockSupersetClient.get.mockResolvedValue({
+ json: { status: 'ready' },
+ } as never);
+
+ render(<MenuWrapper />, { useRedux: true, initialState: loggedInState });
+
+ await userEvent.click(screen.getByText('Export Data to Excel'));
+ await waitFor(() => expect(mockSupersetClient.post).toHaveBeenCalled());
+
+ await act(async () => {
+ jest.advanceTimersByTime(3000);
+ });
+
+ await waitFor(() => {
+ expect(mockAddDangerToast).toHaveBeenCalledWith(
+ 'Sorry, something went wrong. Try again later.',
+ );
+ });
+ expect(mockAddSuccessToast).not.toHaveBeenCalled();
+ expect(mockAddSuccessToast).not.toHaveBeenCalledWith(
+ 'Your export is ready and downloading.',
+ );
+});
+
+test('unmounting stops the polling loop', async () => {
+ jest.useFakeTimers();
+ mockSupersetClient.post.mockResolvedValue({
+ json: { job_id: 'abc' },
+ } as never);
+ mockSupersetClient.get.mockResolvedValue({
+ json: { status: 'pending' },
+ } as never);
+
+ const { unmount } = render(<MenuWrapper />, {
+ useRedux: true,
+ initialState: loggedInState,
+ });
+
+ await userEvent.click(screen.getByText('Export Data to Excel'));
+ await waitFor(() => expect(mockSupersetClient.post).toHaveBeenCalled());
+
+ await act(async () => {
+ jest.advanceTimersByTime(3000);
+ });
+ await waitFor(() => expect(mockSupersetClient.get).toHaveBeenCalledTimes(1));
+
+ unmount();
+
+ await act(async () => {
+ jest.advanceTimersByTime(30000);
+ });
+ expect(mockSupersetClient.get).toHaveBeenCalledTimes(1);
+});
+
+test('the pending toast is announced once, not re-emitted on every poll',
async () => {
+ jest.useFakeTimers();
Review Comment:
This was written against user-event v12 internals; the repo is on v14, and
these tests now click through the `clickMenuItem` helper with `delay: null`, so
nothing waits on a timer under fake timers. The suite passes in CI.
##########
superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadMenuItems.test.tsx:
##########
@@ -368,3 +533,258 @@ test('Enabled screenshot items should not show tooltip
icon', () => {
mockIsFeatureEnabled.mockReset();
});
+
+// ---------------------------------------------------------------------------
+// Delivery follows the requester identity, not iframe presence: a guest or
+// anonymous session (no userId) has no email channel, so the toast must not
+// promise one, and the image export (webdriver-rendered, guests cannot open
+// Explore) is hidden.
+// ---------------------------------------------------------------------------
+
+const guestState = { user: {} };
+
+test('guest session: export toast promises auto-download, not an email', async
() => {
+ mockSupersetClient.post.mockResolvedValue({
+ json: { job_id: 'abc' },
+ } as never);
+
+ render(<MenuWrapper />, { useRedux: true, initialState: guestState });
+
+ await userEvent.click(screen.getByText('Export Data to Excel'));
+
+ await waitFor(() =>
+ expect(mockAddInfoToast).toHaveBeenCalledWith(
+ 'Your export is being generated. Please, do not leave the page.',
+ { noDuplicate: true },
+ ),
+ );
+});
+
+test('guest session: Export Images to Excel is hidden even with the webdriver
enabled', () => {
+ enableWebDriverScreenshot();
+
+ render(<MenuWrapper />, { useRedux: true, initialState: guestState });
+
+ expect(screen.getByText('Export Data to Excel')).toBeInTheDocument();
+ expect(screen.queryByText('Export Images to Excel')).not.toBeInTheDocument();
+});
+
+test('logged-in user without an email gets the delivery-neutral toast', async
() => {
+ mockSupersetClient.post.mockResolvedValue({
+ json: { job_id: 'abc' },
+ } as never);
+
+ render(<MenuWrapper />, {
+ useRedux: true,
+ initialState: { user: { userId: 1 } },
+ });
+
+ await userEvent.click(screen.getByText('Export Data to Excel'));
+
+ await waitFor(() =>
+ expect(mockAddInfoToast).toHaveBeenCalledWith(
+ 'Your export is being generated. Please, do not leave the page.',
+ { noDuplicate: true },
+ ),
+ );
+});
+
+test('a "running" status restarts the wait window, so queue delay is not
counted', async () => {
+ jest.useFakeTimers();
+ mockSupersetClient.post.mockResolvedValue({
+ json: { job_id: 'abc' },
+ } as never);
+ // Worker picks the job up on the first poll; still running just past the
+ // original 12 minute deadline; done on the poll after that.
+ mockSupersetClient.get
+ .mockResolvedValueOnce({ json: { status: 'running' } } as never)
+ .mockResolvedValueOnce({ json: { status: 'running' } } as never)
+ .mockResolvedValueOnce({
+ json: {
+ status: 'ready',
+ download_url: '/api/v1/dashboard/export_xlsx/download/abc/',
+ },
+ } as never);
+
+ render(<MenuWrapper />, { useRedux: true, initialState: loggedInState });
+
+ await userEvent.click(screen.getByText('Export Data to Excel'));
+ await waitFor(() => expect(mockSupersetClient.post).toHaveBeenCalled());
+
+ await act(async () => {
+ jest.advanceTimersByTime(3000);
+ });
+ await waitFor(() => expect(mockSupersetClient.get).toHaveBeenCalledTimes(1));
+
+ // t ~= 12m01s: past the enqueue-based deadline, within the restarted one
+ // (running was observed at t=3s). Without the restart this poll would give
+ // up with a danger toast instead of continuing.
+ await act(async () => {
+ jest.advanceTimersByTime(12 * 60 * 1000 - 2000);
+ });
+ await waitFor(() => expect(mockSupersetClient.get).toHaveBeenCalledTimes(2));
+ expect(mockAddDangerToast).not.toHaveBeenCalled();
+
+ await act(async () => {
+ jest.advanceTimersByTime(3000);
+ });
+ await waitFor(() => {
+
expect(lastIframeSrc()).toBe('/api/v1/dashboard/export_xlsx/download/abc/');
+ expect(mockAddSuccessToast).toHaveBeenCalledWith(
+ 'Your export is ready and downloading.',
+ );
+ });
+});
+
+test('a transient poll failure keeps polling and still downloads', async () =>
{
+ jest.useFakeTimers();
+ mockSupersetClient.post.mockResolvedValue({
+ json: { job_id: 'abc' },
+ } as never);
+ mockSupersetClient.get
+ .mockRejectedValueOnce(new Error('network blip'))
+ .mockResolvedValueOnce({
+ json: {
+ status: 'ready',
+ download_url: '/api/v1/dashboard/export_xlsx/download/abc/',
+ },
+ } as never);
+
+ render(<MenuWrapper />, { useRedux: true, initialState: loggedInState });
+
+ await userEvent.click(screen.getByText('Export Data to Excel'));
+ await waitFor(() => expect(mockSupersetClient.post).toHaveBeenCalled());
+
+ await act(async () => {
+ jest.advanceTimersByTime(3000);
+ });
+ await waitFor(() => expect(mockSupersetClient.get).toHaveBeenCalledTimes(1));
+ expect(mockAddDangerToast).not.toHaveBeenCalled();
+
+ await act(async () => {
+ jest.advanceTimersByTime(3000);
+ });
+ await waitFor(() => {
+
expect(lastIframeSrc()).toBe('/api/v1/dashboard/export_xlsx/download/abc/');
+ expect(mockAddSuccessToast).toHaveBeenCalledWith(
+ 'Your export is ready and downloading.',
+ );
+ });
+});
+
+test('poll failures past the deadline give up with an error toast', async ()
=> {
+ jest.useFakeTimers();
+ mockSupersetClient.post.mockResolvedValue({
+ json: { job_id: 'abc' },
+ } as never);
+ mockSupersetClient.get.mockRejectedValue(new Error('server down'));
+
+ render(<MenuWrapper />, { useRedux: true, initialState: loggedInState });
+
+ await userEvent.click(screen.getByText('Export Data to Excel'));
+ await waitFor(() => expect(mockSupersetClient.post).toHaveBeenCalled());
+
+ await act(async () => {
+ jest.advanceTimersByTime(3000);
+ });
+ await waitFor(() => expect(mockSupersetClient.get).toHaveBeenCalledTimes(1));
+
+ // Jump past the 12 minute deadline; the next failing poll must give up.
+ await act(async () => {
+ jest.advanceTimersByTime(13 * 60 * 1000);
+ });
+ await waitFor(() => {
+ expect(mockAddDangerToast).toHaveBeenCalledWith(
+ 'Sorry, something went wrong. Try again later.',
+ );
+ });
+ expect(mockAddSuccessToast).not.toHaveBeenCalledWith(
+ 'Your export is ready and downloading.',
+ );
+});
+
+test('a "ready" status with no download_url is an error, not a fake success',
async () => {
+ jest.useFakeTimers();
+ mockSupersetClient.post.mockResolvedValue({
+ json: { job_id: 'abc' },
+ } as never);
+ mockSupersetClient.get.mockResolvedValue({
+ json: { status: 'ready' },
+ } as never);
+
+ render(<MenuWrapper />, { useRedux: true, initialState: loggedInState });
+
+ await userEvent.click(screen.getByText('Export Data to Excel'));
+ await waitFor(() => expect(mockSupersetClient.post).toHaveBeenCalled());
+
+ await act(async () => {
+ jest.advanceTimersByTime(3000);
+ });
+
+ await waitFor(() => {
+ expect(mockAddDangerToast).toHaveBeenCalledWith(
+ 'Sorry, something went wrong. Try again later.',
+ );
+ });
+ expect(mockAddSuccessToast).not.toHaveBeenCalled();
+ expect(mockAddSuccessToast).not.toHaveBeenCalledWith(
+ 'Your export is ready and downloading.',
+ );
+});
+
+test('unmounting stops the polling loop', async () => {
+ jest.useFakeTimers();
+ mockSupersetClient.post.mockResolvedValue({
+ json: { job_id: 'abc' },
+ } as never);
+ mockSupersetClient.get.mockResolvedValue({
+ json: { status: 'pending' },
+ } as never);
+
+ const { unmount } = render(<MenuWrapper />, {
+ useRedux: true,
+ initialState: loggedInState,
+ });
+
+ await userEvent.click(screen.getByText('Export Data to Excel'));
+ await waitFor(() => expect(mockSupersetClient.post).toHaveBeenCalled());
+
+ await act(async () => {
+ jest.advanceTimersByTime(3000);
+ });
+ await waitFor(() => expect(mockSupersetClient.get).toHaveBeenCalledTimes(1));
+
+ unmount();
+
+ await act(async () => {
+ jest.advanceTimersByTime(30000);
+ });
+ expect(mockSupersetClient.get).toHaveBeenCalledTimes(1);
+});
+
+test('the pending toast is announced once, not re-emitted on every poll',
async () => {
+ jest.useFakeTimers();
Review Comment:
This was written against user-event v12 internals; the repo is on v14, and
these tests now click through the `clickMenuItem` helper with `delay: null`, so
nothing waits on a timer under fake timers. The suite passes in CI.
##########
superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadMenuItems.test.tsx:
##########
@@ -368,3 +533,258 @@ test('Enabled screenshot items should not show tooltip
icon', () => {
mockIsFeatureEnabled.mockReset();
});
+
+// ---------------------------------------------------------------------------
+// Delivery follows the requester identity, not iframe presence: a guest or
+// anonymous session (no userId) has no email channel, so the toast must not
+// promise one, and the image export (webdriver-rendered, guests cannot open
+// Explore) is hidden.
+// ---------------------------------------------------------------------------
+
+const guestState = { user: {} };
+
+test('guest session: export toast promises auto-download, not an email', async
() => {
+ mockSupersetClient.post.mockResolvedValue({
+ json: { job_id: 'abc' },
+ } as never);
+
+ render(<MenuWrapper />, { useRedux: true, initialState: guestState });
+
+ await userEvent.click(screen.getByText('Export Data to Excel'));
+
+ await waitFor(() =>
+ expect(mockAddInfoToast).toHaveBeenCalledWith(
+ 'Your export is being generated. Please, do not leave the page.',
+ { noDuplicate: true },
+ ),
+ );
+});
+
+test('guest session: Export Images to Excel is hidden even with the webdriver
enabled', () => {
+ enableWebDriverScreenshot();
+
+ render(<MenuWrapper />, { useRedux: true, initialState: guestState });
+
+ expect(screen.getByText('Export Data to Excel')).toBeInTheDocument();
+ expect(screen.queryByText('Export Images to Excel')).not.toBeInTheDocument();
+});
+
+test('logged-in user without an email gets the delivery-neutral toast', async
() => {
+ mockSupersetClient.post.mockResolvedValue({
+ json: { job_id: 'abc' },
+ } as never);
+
+ render(<MenuWrapper />, {
+ useRedux: true,
+ initialState: { user: { userId: 1 } },
+ });
+
+ await userEvent.click(screen.getByText('Export Data to Excel'));
+
+ await waitFor(() =>
+ expect(mockAddInfoToast).toHaveBeenCalledWith(
+ 'Your export is being generated. Please, do not leave the page.',
+ { noDuplicate: true },
+ ),
+ );
+});
+
+test('a "running" status restarts the wait window, so queue delay is not
counted', async () => {
+ jest.useFakeTimers();
+ mockSupersetClient.post.mockResolvedValue({
+ json: { job_id: 'abc' },
+ } as never);
+ // Worker picks the job up on the first poll; still running just past the
+ // original 12 minute deadline; done on the poll after that.
+ mockSupersetClient.get
+ .mockResolvedValueOnce({ json: { status: 'running' } } as never)
+ .mockResolvedValueOnce({ json: { status: 'running' } } as never)
+ .mockResolvedValueOnce({
+ json: {
+ status: 'ready',
+ download_url: '/api/v1/dashboard/export_xlsx/download/abc/',
+ },
+ } as never);
+
+ render(<MenuWrapper />, { useRedux: true, initialState: loggedInState });
+
+ await userEvent.click(screen.getByText('Export Data to Excel'));
+ await waitFor(() => expect(mockSupersetClient.post).toHaveBeenCalled());
+
+ await act(async () => {
+ jest.advanceTimersByTime(3000);
+ });
+ await waitFor(() => expect(mockSupersetClient.get).toHaveBeenCalledTimes(1));
+
+ // t ~= 12m01s: past the enqueue-based deadline, within the restarted one
+ // (running was observed at t=3s). Without the restart this poll would give
+ // up with a danger toast instead of continuing.
+ await act(async () => {
+ jest.advanceTimersByTime(12 * 60 * 1000 - 2000);
+ });
+ await waitFor(() => expect(mockSupersetClient.get).toHaveBeenCalledTimes(2));
+ expect(mockAddDangerToast).not.toHaveBeenCalled();
+
+ await act(async () => {
+ jest.advanceTimersByTime(3000);
+ });
+ await waitFor(() => {
+
expect(lastIframeSrc()).toBe('/api/v1/dashboard/export_xlsx/download/abc/');
+ expect(mockAddSuccessToast).toHaveBeenCalledWith(
+ 'Your export is ready and downloading.',
+ );
+ });
+});
+
+test('a transient poll failure keeps polling and still downloads', async () =>
{
+ jest.useFakeTimers();
+ mockSupersetClient.post.mockResolvedValue({
+ json: { job_id: 'abc' },
+ } as never);
+ mockSupersetClient.get
+ .mockRejectedValueOnce(new Error('network blip'))
+ .mockResolvedValueOnce({
+ json: {
+ status: 'ready',
+ download_url: '/api/v1/dashboard/export_xlsx/download/abc/',
+ },
+ } as never);
+
+ render(<MenuWrapper />, { useRedux: true, initialState: loggedInState });
+
+ await userEvent.click(screen.getByText('Export Data to Excel'));
+ await waitFor(() => expect(mockSupersetClient.post).toHaveBeenCalled());
+
+ await act(async () => {
+ jest.advanceTimersByTime(3000);
+ });
+ await waitFor(() => expect(mockSupersetClient.get).toHaveBeenCalledTimes(1));
+ expect(mockAddDangerToast).not.toHaveBeenCalled();
+
+ await act(async () => {
+ jest.advanceTimersByTime(3000);
+ });
+ await waitFor(() => {
+
expect(lastIframeSrc()).toBe('/api/v1/dashboard/export_xlsx/download/abc/');
+ expect(mockAddSuccessToast).toHaveBeenCalledWith(
+ 'Your export is ready and downloading.',
+ );
+ });
+});
+
+test('poll failures past the deadline give up with an error toast', async ()
=> {
+ jest.useFakeTimers();
+ mockSupersetClient.post.mockResolvedValue({
+ json: { job_id: 'abc' },
+ } as never);
+ mockSupersetClient.get.mockRejectedValue(new Error('server down'));
+
+ render(<MenuWrapper />, { useRedux: true, initialState: loggedInState });
+
+ await userEvent.click(screen.getByText('Export Data to Excel'));
+ await waitFor(() => expect(mockSupersetClient.post).toHaveBeenCalled());
+
+ await act(async () => {
+ jest.advanceTimersByTime(3000);
+ });
+ await waitFor(() => expect(mockSupersetClient.get).toHaveBeenCalledTimes(1));
+
+ // Jump past the 12 minute deadline; the next failing poll must give up.
+ await act(async () => {
+ jest.advanceTimersByTime(13 * 60 * 1000);
+ });
+ await waitFor(() => {
+ expect(mockAddDangerToast).toHaveBeenCalledWith(
+ 'Sorry, something went wrong. Try again later.',
+ );
+ });
+ expect(mockAddSuccessToast).not.toHaveBeenCalledWith(
+ 'Your export is ready and downloading.',
+ );
+});
+
+test('a "ready" status with no download_url is an error, not a fake success',
async () => {
+ jest.useFakeTimers();
+ mockSupersetClient.post.mockResolvedValue({
+ json: { job_id: 'abc' },
+ } as never);
+ mockSupersetClient.get.mockResolvedValue({
+ json: { status: 'ready' },
+ } as never);
+
+ render(<MenuWrapper />, { useRedux: true, initialState: loggedInState });
+
+ await userEvent.click(screen.getByText('Export Data to Excel'));
+ await waitFor(() => expect(mockSupersetClient.post).toHaveBeenCalled());
+
+ await act(async () => {
+ jest.advanceTimersByTime(3000);
+ });
+
+ await waitFor(() => {
+ expect(mockAddDangerToast).toHaveBeenCalledWith(
+ 'Sorry, something went wrong. Try again later.',
+ );
+ });
+ expect(mockAddSuccessToast).not.toHaveBeenCalled();
+ expect(mockAddSuccessToast).not.toHaveBeenCalledWith(
+ 'Your export is ready and downloading.',
+ );
+});
+
+test('unmounting stops the polling loop', async () => {
+ jest.useFakeTimers();
+ mockSupersetClient.post.mockResolvedValue({
+ json: { job_id: 'abc' },
+ } as never);
+ mockSupersetClient.get.mockResolvedValue({
+ json: { status: 'pending' },
+ } as never);
+
+ const { unmount } = render(<MenuWrapper />, {
+ useRedux: true,
+ initialState: loggedInState,
+ });
+
+ await userEvent.click(screen.getByText('Export Data to Excel'));
+ await waitFor(() => expect(mockSupersetClient.post).toHaveBeenCalled());
+
+ await act(async () => {
+ jest.advanceTimersByTime(3000);
+ });
+ await waitFor(() => expect(mockSupersetClient.get).toHaveBeenCalledTimes(1));
+
+ unmount();
+
+ await act(async () => {
+ jest.advanceTimersByTime(30000);
+ });
+ expect(mockSupersetClient.get).toHaveBeenCalledTimes(1);
+});
+
+test('the pending toast is announced once, not re-emitted on every poll',
async () => {
+ jest.useFakeTimers();
Review Comment:
This was written against user-event v12 internals; the repo is on v14, and
these tests now click through the `clickMenuItem` helper with `delay: null`, so
nothing waits on a timer under fake timers. The suite passes in CI.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]