Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SLVUU-117 add type guard for layoutJSON #133

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
1,187 changes: 902 additions & 285 deletions vuu-ui/package-lock.json

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions vuu-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@
},
"dependencies": {
"@salt-ds/core": "1.13.2",
"@types/jest": "^26.0.20",
"@testing-library/react-hooks": "^8.0.1",
"@types/jest": "^29.5.11",
"@types/node": "^18.0.0",
"@types/react": "^17.0.2",
"@types/react-dom": "^17.0.2",
Expand Down Expand Up @@ -77,7 +78,7 @@
"eslint-plugin-prettier": "4.2.1",
"eslint-plugin-react": "7.32.2",
"eslint-plugin-react-hooks": "4.6.0",
"happy-dom": "^10.10.0",
"happy-dom": "^12.10.3",
"open": "10.0.0",
"prettier": "2.8.4",
"serve": "^14.2.1",
Expand Down
5 changes: 4 additions & 1 deletion vuu-ui/packages/vuu-layout/src/utils/typeOf.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ReactElement } from 'react';
import { LayoutModel, WithType } from '../layout-reducer';
import { LayoutJSON, LayoutModel, WithType } from '../layout-reducer';

export function typeOf(element?: LayoutModel | WithType): string | undefined {
if (element) {
Expand All @@ -23,3 +23,6 @@ export function typeOf(element?: LayoutModel | WithType): string | undefined {
}

export const isTypeOf = (element: ReactElement, type: string) => typeOf(element) === type;

export const isLayoutJSON = (layout: LayoutJSON): layout is LayoutJSON =>
layout !== undefined && "type" in layout;
30 changes: 30 additions & 0 deletions vuu-ui/packages/vuu-layout/test/typeOf.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { describe, expect, it } from "vitest";
import { LayoutJSON, isLayoutJSON } from "../src";

const validLayout: LayoutJSON = {
type: "Stack",
};

const invalidLayout: unknown = {
name: "invalid-layout-test",
};
pling-scottlogic marked this conversation as resolved.
Show resolved Hide resolved

describe("isLayoutJSON", () => {
it("returns true when layout is valid", () => {
const result = isLayoutJSON(validLayout);

expect(result).toBe(true);
});

it("returns false when layout is not valid", () => {
const result = isLayoutJSON(invalidLayout as LayoutJSON);

expect(result).toBe(false);
});

it("returns false when layout is undefined", () => {
const result = isLayoutJSON(undefined as unknown as LayoutJSON);

expect(result).toBe(false);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
LayoutJSON,
resolveJSONPath,
ApplicationSetting,
isLayoutJSON,
} from "@finos/vuu-layout";
import { NotificationLevel, useNotifications } from "@finos/vuu-popups";
import { LayoutMetadata, LayoutMetadataDto } from "./layoutTypes";
Expand Down Expand Up @@ -164,8 +165,12 @@ export const LayoutManagementProvider = (

const saveApplicationLayout = useCallback(
(layout: LayoutJSON) => {
setApplicationLayout(layout, false);
getPersistenceManager().saveApplicationJSON(applicationJSONRef.current);
if (isLayoutJSON(layout)) {
setApplicationLayout(layout, false);
getPersistenceManager().saveApplicationJSON(applicationJSONRef.current);
} else {
console.error("Tried to save invalid application layout", layout);
}
},
[setApplicationLayout]
);
Expand All @@ -177,7 +182,7 @@ export const LayoutManagementProvider = (
"#main-tabs.ACTIVE_CHILD"
);

if (layoutToSave) {
if (layoutToSave && isLayoutJSON(layoutToSave)) {
getPersistenceManager()
.createLayout(metadata, ensureLayoutHasTitle(layoutToSave, metadata))
.then((metadata) => {
Expand All @@ -197,10 +202,11 @@ export const LayoutManagementProvider = (
console.error("Error occurred while saving layout", error);
});
} else {
console.error("Tried to save invalid layout", layoutToSave);
notify({
type: NotificationLevel.Error,
header: "Failed to Save Layout",
body: "Cannot save undefined layout",
body: "Cannot save invalid layout",
});
}
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
import React from "react";
import "@finos/vuu-layout/test/global-mocks";
import { renderHook } from "@testing-library/react-hooks";
import { beforeEach, afterEach, describe, expect, it, vi } from "vitest";

import { LayoutJSON, isLayoutJSON, resolveJSONPath } from "@finos/vuu-layout";

import { useNotifications } from "@finos/vuu-popups";

import {
LayoutManagementProvider,
LayoutMetadata,
useLayoutManager,
} from "../../src";

import {
LocalPersistenceManager,
PersistenceManager,
} from "../../src/persistence-management";

const defaultLayout = vi.hoisted(() => ({
type: "Stack",
title: "test-layout",
}));

const newLayout: LayoutJSON = {
type: "Stack",
props: {
title: "test-layout-edited",
},
};

const metadata: LayoutMetadata = {
name: "test-name",
group: "test-group",
screenshot: "test-screenshot",
user: "test-user",
created: "test-date",
id: "test-id",
};

const initialApplicationJSON = vi.hoisted(() => ({
layout: defaultLayout,
}));

vi.stubEnv("LOCAL", "true");

vi.mock(
"../../src/persistence-management/LocalPersistenceManager",
async () => {
const MockPersistenceManager = vi.fn();
MockPersistenceManager.prototype.loadMetadata = vi.fn();
MockPersistenceManager.prototype.loadApplicationJSON = vi.fn();
MockPersistenceManager.prototype.saveApplicationJSON = vi.fn();
MockPersistenceManager.prototype.createLayout = vi.fn();
MockPersistenceManager.prototype.updateLayout = vi.fn();
MockPersistenceManager.prototype.deleteLayout = vi.fn();
MockPersistenceManager.prototype.loadLayout = vi.fn();

return { LocalPersistenceManager: MockPersistenceManager };
}
);

vi.mock("../../src/persistence-management/defaultApplicationJson", async () => {
const actual = await vi.importActual<
typeof import("../../src/persistence-management/defaultApplicationJson")
>("../../src/persistence-management/defaultApplicationJson");
return {
...actual,
loadingApplicationJson: initialApplicationJSON,
};
});

vi.mock("@finos/vuu-popups", async () => {
const actual = await vi.importActual<typeof import("@finos/vuu-popups")>(
"@finos/vuu-popups"
);
const mockNotify = vi.fn();
return {
...actual,
useNotifications: vi.fn(() => ({ notify: mockNotify })),
};
});

vi.mock("@finos/vuu-layout", async () => {
const actual = await vi.importActual<typeof import("@finos/vuu-layout")>(
"@finos/vuu-layout"
);
return {
...actual,
isLayoutJSON: vi.fn(),
resolveJSONPath: vi.fn(),
};
});

const wrapper = ({ children }) => (
<LayoutManagementProvider>{children}</LayoutManagementProvider>
);

describe("LayoutManagementProvider", () => {
let persistence: PersistenceManager;

beforeEach(() => {
persistence = new LocalPersistenceManager();
vi.mocked(persistence.loadMetadata).mockResolvedValueOnce([]);
vi.mocked(persistence.loadApplicationJSON).mockResolvedValueOnce(
initialApplicationJSON
);
vi.spyOn(global.console, "error");
});

afterEach(() => {
vi.restoreAllMocks();
});

it("calls loadMetadata and loadApplicationJSON on mount", () => {
const { result } = renderHook(() => useLayoutManager(), { wrapper });

expect(persistence.loadMetadata).toHaveBeenCalled();
expect(persistence.loadApplicationJSON).toHaveBeenCalled();
expect(result.current.applicationJson).toBe(initialApplicationJSON);
});

describe("saveApplicationLayout", () => {
it("calls saveApplicationJSON when layout is valid", () => {
const { result } = renderHook(useLayoutManager, { wrapper });

vi.mocked(persistence.saveApplicationJSON).mockResolvedValueOnce();
vi.mocked(isLayoutJSON).mockReturnValue(true);
pling-scottlogic marked this conversation as resolved.
Show resolved Hide resolved

result.current.saveApplicationLayout(newLayout);

expect(persistence.saveApplicationJSON).toHaveBeenCalledWith({
...initialApplicationJSON,
layout: newLayout,
});
});

it("doesn't call saveApplicationJSON and logs error when layout is not valid ", () => {
const { result } = renderHook(() => useLayoutManager(), { wrapper });

vi.mocked(persistence.saveApplicationJSON).mockResolvedValueOnce();
vi.mocked(isLayoutJSON).mockReturnValue(false);

result.current.saveApplicationLayout(newLayout);

expect(persistence.saveApplicationJSON).not.toHaveBeenCalled();
expect(console.error).toHaveBeenCalledWith(
"Tried to save invalid application layout",
newLayout
);
});
});

describe("saveLayout", () => {
it("calls createLayout when layout is valid and path is resolved", () => {
const { result } = renderHook(() => useLayoutManager(), { wrapper });

vi.mocked(persistence.createLayout).mockResolvedValueOnce(metadata);
vi.mocked(isLayoutJSON).mockReturnValue(true);
vi.mocked(resolveJSONPath).mockReturnValue(newLayout);

result.current.saveLayout(metadata);

expect(persistence.createLayout).toHaveBeenCalledWith(
metadata,
newLayout
);
});

it("doesn't call createLayout, triggers error notification and logs error when layout path can't be resolved ", () => {
const { result } = renderHook(() => useLayoutManager(), { wrapper });
const { notify } = useNotifications();

vi.mocked(persistence.createLayout).mockResolvedValueOnce(metadata);
vi.mocked(resolveJSONPath).mockReturnValue(undefined);
pling-scottlogic marked this conversation as resolved.
Show resolved Hide resolved
vi.mocked(isLayoutJSON).mockReturnValue(true);

result.current.saveLayout(metadata);

expect(persistence.createLayout).not.toHaveBeenCalled();
expect(notify).toHaveBeenCalledWith({
body: "Cannot save invalid layout",
header: "Failed to Save Layout",
type: "error",
});
expect(console.error).toHaveBeenCalledWith(
"Tried to save invalid layout",
undefined
);
});

it("doesn't call createLayout, triggers error notification and logs error when layout is not valid", () => {
const { result } = renderHook(() => useLayoutManager(), { wrapper });
const { notify } = useNotifications();

vi.mocked(persistence.createLayout).mockResolvedValueOnce(metadata);
vi.mocked(isLayoutJSON).mockReturnValue(false);
vi.mocked(resolveJSONPath).mockReturnValue(defaultLayout);

result.current.saveLayout(metadata);

expect(persistence.createLayout).not.toHaveBeenCalled();
expect(notify).toHaveBeenCalledWith({
body: "Cannot save invalid layout",
header: "Failed to Save Layout",
type: "error",
});
expect(console.error).toHaveBeenCalledWith(
"Tried to save invalid layout",
defaultLayout
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { LayoutJSON } from "@finos/vuu-layout";
import {
getLocalEntity,
saveLocalEntity,
} from "../../../vuu-filters/src/local-config";
} from "@finos/vuu-filters/src/local-config";
import { formatDate } from "@finos/vuu-utils";
import { expectPromiseRejectsWithError } from "@finos/vuu-utils/test/utils";
import { LocalPersistenceManager } from "../../src/persistence-management/LocalPersistenceManager";
Expand Down
2 changes: 1 addition & 1 deletion vuu-ui/vitest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { defineConfig } from "vitest/config";

export default defineConfig({
test: {
include: ["packages/**/test/**/**.test.(js|ts)"],
include: ["packages/**/test/**/**.test.(js|ts|tsx)"],
environment: 'happy-dom',
},
});
Loading