Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
7c08eef
Basic file uploading
mikaylathompson Apr 26, 2025
f518b71
Improve error checking/display
mikaylathompson Apr 26, 2025
b06d1b9
Add code view popover
mikaylathompson Apr 26, 2025
35eba9d
format content before displaying
mikaylathompson Apr 26, 2025
60ab771
Add eslint rule and fixes for relative imports
mikaylathompson Apr 26, 2025
2e2bdbc
Refactor
mikaylathompson Apr 26, 2025
769b07c
Add delete option
mikaylathompson Apr 26, 2025
b3fa40e
Add tests
mikaylathompson Apr 26, 2025
9b52d58
Change ndjson handling (separate docs), update tests
mikaylathompson Apr 26, 2025
deed872
Deal with size limits for local storage
mikaylathompson Apr 26, 2025
592f33c
Add editing feature
mikaylathompson Apr 26, 2025
140748c
rough draft of transformation panel
mikaylathompson Apr 29, 2025
0e446ad
mostly working version of editors
mikaylathompson Apr 29, 2025
35f0be2
Fix the twitchiness and invisible highlighting
mikaylathompson Apr 30, 2025
51214ec
Formatting, save status, etc.
mikaylathompson Apr 30, 2025
7c5f98d
Handle BoardItem resizing for the editor
mikaylathompson Apr 30, 2025
91529e3
Default content, cleanup, sizing
mikaylathompson Apr 30, 2025
491ed39
Merge branch 'main' into playground-transformations
mikaylathompson Apr 30, 2025
264d068
Sonarqube fixes
mikaylathompson Apr 30, 2025
308127a
Fix editor bugs (erasing changes) and clearer error display
mikaylathompson May 1, 2025
89ca8a6
Linter fixes
mikaylathompson May 2, 2025
4ac6a2e
Add missing files
mikaylathompson May 2, 2025
28d66cd
sonarqube fixes
mikaylathompson May 2, 2025
1b3aefc
Incorporate review feedback
mikaylathompson May 2, 2025
2ee8b96
Remove all custom styles in favor of Cloudspace components
mikaylathompson May 2, 2025
1a60b63
Add SaveStatusIndicator tests
mikaylathompson May 2, 2025
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import "@testing-library/jest-dom";
import SaveStatusIndicator from "@/components/playground/SaveStatusIndicator";
import { SaveStatus } from "@/types/SaveStatus";
import { formatDistanceToNow } from "date-fns";
import { IAnnotation } from "react-ace/lib/types";

// Make mocks for the external libraries
jest.mock("date-fns", () => ({
formatDistanceToNow: jest.fn(),
}));
jest.mock("@cloudscape-design/components/status-indicator", () => {
return {
__esModule: true,
default: ({
type,
children,
}: {
type: string;
children: React.ReactNode;
}) => (
<div data-testid="status-indicator" data-type={type}>
{children}
</div>
),
};
});

describe("SaveStatusIndicator", () => {
beforeEach(() => {
jest.clearAllMocks();
});

describe("status indicator display based off of state", () => {
it("should render with success styling when status is SAVED", () => {
const savedState = {
status: SaveStatus.SAVED,
savedAt: null,
errors: [],
};

render(<SaveStatusIndicator state={savedState} />);
const statusIndicator = screen.getByTestId("status-indicator");

expect(statusIndicator).toBeInTheDocument();
expect(statusIndicator).toHaveAttribute("data-type", "success");
expect(statusIndicator).toHaveTextContent("Saved");
});

it("should render with error styling when status is BLOCKED", () => {
const blockedState = {
status: SaveStatus.BLOCKED,
savedAt: null,
errors: [],
};

render(<SaveStatusIndicator state={blockedState} />);
const statusIndicator = screen.getByTestId("status-indicator");

expect(statusIndicator).toBeInTheDocument();
expect(statusIndicator).toHaveAttribute("data-type", "error");
expect(statusIndicator).toHaveTextContent("Blocked");
});

it("should render with in-progress styling when status is UNSAVED", () => {
const unsavedState = {
status: SaveStatus.UNSAVED,
savedAt: null,
errors: [],
};

render(<SaveStatusIndicator state={unsavedState} />);
const statusIndicator = screen.getByTestId("status-indicator");

expect(statusIndicator).toBeInTheDocument();
expect(statusIndicator).toHaveAttribute("data-type", "in-progress");
expect(statusIndicator).toHaveTextContent("Unsaved");
});

it("should default to in-progress styling when state is undefined", () => {
// @ts-ignore - Intentionally passing undefined for testing
render(<SaveStatusIndicator state={undefined} />);
const statusIndicator = screen.getByTestId("status-indicator");

expect(statusIndicator).toBeInTheDocument();
expect(statusIndicator).toHaveAttribute("data-type", "in-progress");
expect(statusIndicator).toHaveTextContent("Unsaved");
});

it("should default to in-progress styling when state.status is null", () => {
const nullState = {
status: null,
savedAt: null,
errors: [],
};

// @ts-ignore - Intentionally passing null status for testing
render(<SaveStatusIndicator state={nullState} />);
const statusIndicator = screen.getByTestId("status-indicator");

expect(statusIndicator).toBeInTheDocument();
expect(statusIndicator).toHaveAttribute("data-type", "in-progress");
expect(statusIndicator).toHaveTextContent("Unsaved");
});
});

describe("save status indicator displaying time formatting", () => {
it("should display the correct time format when savedAt is provided", () => {
const mockTime = "5 minutes";
(formatDistanceToNow as jest.Mock).mockReturnValue(mockTime);

const savedDate = new Date();
const savedState = {
status: SaveStatus.SAVED,
savedAt: savedDate,
errors: [],
};

render(<SaveStatusIndicator state={savedState} />);

expect(formatDistanceToNow).toHaveBeenCalledWith(savedDate);
expect(screen.getByText(`Saved ${mockTime} ago`)).toBeInTheDocument();
});

it("should not display time when savedAt is null", () => {
const savedState = {
status: SaveStatus.SAVED,
savedAt: null,
errors: [],
};

render(<SaveStatusIndicator state={savedState} />);

expect(formatDistanceToNow).not.toHaveBeenCalled();
expect(screen.getByText("Saved")).toBeInTheDocument();
expect(screen.queryByText(/ago/)).not.toBeInTheDocument();
});
});

describe("save status indicator displaying error messages", () => {
it("should display correct message with single error", () => {
const blockedState = {
status: SaveStatus.BLOCKED,
savedAt: null,
errors: [
{ row: 1, column: 1, type: "error" as const, text: "Test error" },
] as IAnnotation[],
};

render(<SaveStatusIndicator state={blockedState} />);

expect(screen.getByText("Blocked (1 error)")).toBeInTheDocument();
});

it("should display correct message with multiple errors", () => {
const blockedState = {
status: SaveStatus.BLOCKED,
savedAt: null,
errors: [
{ row: 1, column: 1, type: "error" as const, text: "First error" },
{ row: 2, column: 1, type: "error" as const, text: "Second error" },
{ row: 3, column: 1, type: "error" as const, text: "Third error" },
] as IAnnotation[],
};

render(<SaveStatusIndicator state={blockedState} />);

expect(screen.getByText("Blocked (3 errors)")).toBeInTheDocument();
});

it("should display only 'Blocked' when there are no errors", () => {
const blockedState = {
status: SaveStatus.BLOCKED,
savedAt: null,
errors: [],
};

render(<SaveStatusIndicator state={blockedState} />);

expect(screen.getByText("Blocked")).toBeInTheDocument();
expect(screen.queryByText(/\(\d+ errors?\)/)).not.toBeInTheDocument();
});
});
});
12 changes: 7 additions & 5 deletions frontend/__tests__/hooks/usePlaygroundActions.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,13 @@ describe("usePlaygroundActions", () => {
const { result } = renderHook(() => usePlaygroundActions(), { wrapper });

// Adding a document should throw an error
expect(() => {
act(() => {
result.current.addInputDocument(TEST_DOC_NAME, TEST_DOC_CONTENT);
});
}).toThrow(/exceed the maximum storage limit/);
const addDocumentAction = () => {
result.current.addInputDocument(TEST_DOC_NAME, TEST_DOC_CONTENT);
};

expect(() => act(addDocumentAction)).toThrow(
/exceed the maximum storage limit/
);

// Reset the mock
(getJsonSizeInBytes as jest.Mock).mockReturnValue(1024);
Expand Down
8 changes: 6 additions & 2 deletions frontend/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,15 @@ const nextConfig: NextConfig = {
"@cloudscape-design/component-toolkit"
],
webpack(config) {
// h/t https://github.com/securingsincity/react-ace/issues/725#issuecomment-1407356137
// Configure webpack to handle Ace Editor worker files
config.module.rules.push({
test: /ace-builds.*\/worker-.*$/,
test: /ace-builds.*\/worker-.*\.js$/,
type: "asset/resource",
generator: {
filename: "static/workers/[name][ext]",
},
});

return config;
},
};
Expand Down
71 changes: 60 additions & 11 deletions frontend/package-lock.json

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

2 changes: 2 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@
"@cloudscape-design/test-utils-core": "^1.0.56",
"@types/uuid": "^10.0.0",
"ace-builds": "^1.40.1",
"date-fns": "^4.1.0",
"next": "15.3.0",
"react": "^18.3.1",
"react-ace": "^14.0.1",
"react-dom": "^18.3.1",
"uuid": "^11.1.0"
},
Expand Down
Loading
Loading