diff --git a/pkg/ui/pnpm-lock.yaml b/pkg/ui/pnpm-lock.yaml index b7f8afd2df1e..85a6d1277a36 100644 --- a/pkg/ui/pnpm-lock.yaml +++ b/pkg/ui/pnpm-lock.yaml @@ -248,6 +248,9 @@ importers: '@testing-library/dom': specifier: ^8.11.1 version: 8.11.1 + '@testing-library/jest-dom': + specifier: 6.5.0 + version: 6.5.0 '@testing-library/react': specifier: ^12.1.0 version: 12.1.0(react-dom@16.12.0)(react@16.12.0) @@ -1195,6 +1198,10 @@ packages: resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} engines: {node: '>=0.10.0'} + /@adobe/css-tools@4.4.0: + resolution: {integrity: sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==} + dev: true + /@ampproject/remapping@2.2.1: resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} engines: {node: '>=6.0.0'} @@ -7460,6 +7467,19 @@ packages: pretty-format: 27.3.1(@babel/runtime@7.12.13) dev: true + /@testing-library/jest-dom@6.5.0: + resolution: {integrity: sha512-xGGHpBXYSHUUr6XsKBfs85TWlYKpTc37cSBBVrXcib2MkHLboWlkClhWF37JKlDb9KEq3dHs+f2xR7XJEWGBxA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + dependencies: + '@adobe/css-tools': 4.4.0 + aria-query: 5.0.1 + chalk: 3.0.0 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + lodash: 4.17.20 + redent: 3.0.0 + dev: true + /@testing-library/react@12.1.0(react-dom@16.12.0)(react@16.12.0): resolution: {integrity: sha512-Ge3Ht3qXE82Yv9lyPpQ7ZWgzo/HgOcHu569Y4ZGWcZME38iOFiOg87qnu6hTEa8jTJVL7zYovnvD3GE2nsNIoQ==} engines: {node: '>=12'} @@ -11479,6 +11499,10 @@ packages: engines: {node: '>= 6'} dev: true + /css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + dev: true + /cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -12108,6 +12132,10 @@ packages: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} dev: true + /dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + dev: true + /dom-align@1.12.4: resolution: {integrity: sha512-R8LUSEay/68zE5c8/3BDxiTEvgb4xZTF0RKmAHfiEVN3klfIpXfi2/QCoiWPccVQ0J/ZGdz9OjzL4uJEP/MRAw==} dev: false @@ -20997,6 +21025,14 @@ packages: minimatch: 3.0.4 dev: true + /redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + dev: true + /reduce-css-calc@2.1.8: resolution: {integrity: sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg==} dependencies: diff --git a/pkg/ui/workspaces/cluster-ui/package.json b/pkg/ui/workspaces/cluster-ui/package.json index 8f7b498b76c0..40988c27d0a2 100644 --- a/pkg/ui/workspaces/cluster-ui/package.json +++ b/pkg/ui/workspaces/cluster-ui/package.json @@ -97,6 +97,7 @@ "@storybook/addons": "^6.3.1", "@storybook/react": "^6.3.1", "@testing-library/dom": "^8.11.1", + "@testing-library/jest-dom": "6.5.0", "@testing-library/react": "^12.1.0", "@testing-library/user-event": "^13.5.0", "@types/chai": "^4.2.11", diff --git a/pkg/ui/workspaces/cluster-ui/src/components/tableMetadataLastUpdated/tableMetadataJobControl.spec.tsx b/pkg/ui/workspaces/cluster-ui/src/components/tableMetadataLastUpdated/tableMetadataJobControl.spec.tsx new file mode 100644 index 000000000000..817a050bb75f --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/components/tableMetadataLastUpdated/tableMetadataJobControl.spec.tsx @@ -0,0 +1,178 @@ +// Copyright 2024 The Cockroach Authors. +// +// Use of this software is governed by the CockroachDB Software License +// included in the /LICENSE file. + +import "@testing-library/jest-dom"; +import { render, screen, fireEvent, act } from "@testing-library/react"; +import moment from "moment-timezone"; +import React from "react"; + +import * as api from "src/api/databases/tableMetaUpdateJobApi"; +import { TimezoneContext } from "src/contexts"; + +import { TableMetadataJobControl } from "./tableMetadataJobControl"; + +jest.mock("src/api/databases/tableMetaUpdateJobApi"); + +describe("TableMetadataJobControl", () => { + const mockOnDataUpdated = jest.fn(); + const mockRefreshJobStatus = jest.fn(); + const mockLastCompletedTime = moment("2024-01-01T12:00:00Z"); + const mockLastUpdatedTime = moment("2024-01-01T12:05:00Z"); + const mockLastStartTime = moment("2024-01-01T11:59:00Z"); + + beforeEach(() => { + jest.useFakeTimers(); + jest.spyOn(api, "useTableMetaUpdateJob").mockReturnValue({ + jobStatus: { + dataValidDuration: moment.duration(1, "hour"), + currentStatus: api.TableMetadataJobStatus.NOT_RUNNING, + progress: 0, + lastCompletedTime: mockLastCompletedTime, + lastStartTime: mockLastStartTime, + lastUpdatedTime: mockLastUpdatedTime, + automaticUpdatesEnabled: false, + }, + refreshJobStatus: mockRefreshJobStatus, + isLoading: false, + }); + jest.spyOn(api, "triggerUpdateTableMetaJobApi").mockResolvedValue({ + message: "Job triggered", + job_triggered: true, + }); + }); + + afterEach(() => { + jest.useRealTimers(); + jest.clearAllMocks(); + }); + + it("renders the last refreshed time", () => { + render( + + + , + ); + + expect(screen.getByText(/Last refreshed:/)).toBeInTheDocument(); + expect( + screen.getByText(/Jan 01, 2024 at 12:00:00 UTC/), + ).toBeInTheDocument(); + }); + + it('renders "Never" when lastCompletedTime is null', () => { + jest.spyOn(api, "useTableMetaUpdateJob").mockReturnValue({ + jobStatus: { + lastCompletedTime: null, + dataValidDuration: moment.duration(1, "hour"), + currentStatus: api.TableMetadataJobStatus.NOT_RUNNING, + progress: 0, + lastStartTime: mockLastUpdatedTime, + lastUpdatedTime: mockLastUpdatedTime, + automaticUpdatesEnabled: false, + }, + refreshJobStatus: mockRefreshJobStatus, + isLoading: false, + }); + + render( + + + , + ); + + expect(screen.getByText(/Last refreshed: Never/)).toBeInTheDocument(); + }); + + it("triggers update when refresh button is clicked", async () => { + render( + + + , + ); + + const refreshButton = screen.getByRole("button"); + await act(async () => { + fireEvent.click(refreshButton); + }); + + expect(api.triggerUpdateTableMetaJobApi).toHaveBeenCalledWith({ + onlyIfStale: false, + }); + expect(mockRefreshJobStatus).toHaveBeenCalled(); + }); + + it("schedules next update after dataValidDuration", async () => { + render( + + + , + ); + + await act(async () => { + // Advance timer 1 hour and 30s. + jest.advanceTimersByTime(3600000 + 30000); + }); + + expect(api.triggerUpdateTableMetaJobApi).toHaveBeenCalledWith({ + onlyIfStale: true, + }); + }); + + it("calls onDataUpdated when lastCompletedTime changes", () => { + const { rerender } = render( + + + , + ); + + // Update the mock to return a new lastCompletedTime + jest.spyOn(api, "useTableMetaUpdateJob").mockReturnValue({ + jobStatus: { + lastCompletedTime: moment("2024-01-01T13:00:00Z"), + lastStartTime: moment("2024-01-01T13:00:00Z"), + lastUpdatedTime: moment.utc(), + dataValidDuration: moment.duration(1, "hour"), + currentStatus: api.TableMetadataJobStatus.NOT_RUNNING, + progress: 0, + automaticUpdatesEnabled: false, + }, + refreshJobStatus: mockRefreshJobStatus, + isLoading: false, + }); + + // Rerender the component with the updated mock + rerender( + + + , + ); + + expect(mockOnDataUpdated).toHaveBeenCalled(); + }); + + it("disables refresh button when job is running", () => { + jest.spyOn(api, "useTableMetaUpdateJob").mockReturnValue({ + jobStatus: { + lastCompletedTime: moment("2024-01-01T12:00:00Z"), + dataValidDuration: moment.duration(1, "hour"), + currentStatus: api.TableMetadataJobStatus.RUNNING, + progress: 0, + lastStartTime: moment.utc(), + lastUpdatedTime: moment.utc(), + automaticUpdatesEnabled: false, + }, + refreshJobStatus: mockRefreshJobStatus, + isLoading: false, + }); + + render( + + + , + ); + + expect(screen.getByRole("button")).toBeDisabled(); + }); +}); diff --git a/pkg/ui/workspaces/cluster-ui/src/components/tableMetadataLastUpdated/tableMetadataJobControl.tsx b/pkg/ui/workspaces/cluster-ui/src/components/tableMetadataLastUpdated/tableMetadataJobControl.tsx index 8f4e3233ada1..05f43ff91bff 100644 --- a/pkg/ui/workspaces/cluster-ui/src/components/tableMetadataLastUpdated/tableMetadataJobControl.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/components/tableMetadataLastUpdated/tableMetadataJobControl.tsx @@ -34,7 +34,7 @@ export const TableMetadataJobControl: React.FC< ); const lastUpdateCompletedUnixSecs = jobStatus?.lastCompletedTime?.unix(); const timezone = useContext(TimezoneContext); - const lastUpdatedText = jobStatus?.lastUpdatedTime + const lastUpdatedText = jobStatus?.lastCompletedTime ? FormatWithTimezone( jobStatus?.lastCompletedTime, DATE_WITH_SECONDS_FORMAT_24_TZ,