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

N21-1319 ctl showing tool usage #2922

Merged
merged 18 commits into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from 12 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
143 changes: 132 additions & 11 deletions src/components/administration/ExternalToolSection.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,25 @@ import createComponentMocks from "@@/tests/test-utils/componentMocks";
import { i18nMock } from "@@/tests/test-utils/i18nMock";
import { mdiCheckCircle, mdiRefreshCircle } from "@mdi/js";
import { mount, Wrapper } from "@vue/test-utils";
import Vue from "vue";
import Vue, { ref } from "vue";
import ExternalToolSection from "./ExternalToolSection.vue";
import { createMock, DeepMocked } from "@golevelup/ts-jest";
import { useSchoolExternalToolUsage } from "@data-external-tool";
import {
schoolExternalToolFactory,
schoolExternalToolMetadataFactory,
} from "@@/tests/test-utils/factory";

jest.mock("@data-external-tool");

describe("ExternalToolSection", () => {
let el: HTMLDivElement;

const setup = (getters: Partial<SchoolExternalToolsModule> = {}) => {
let useSchoolExternalToolUsageMock: DeepMocked<
ReturnType<typeof useSchoolExternalToolUsage>
>;

const getWrapper = (getters: Partial<SchoolExternalToolsModule> = {}) => {
el = document.createElement("div");
el.setAttribute("data-app", "true");
document.body.appendChild(el);
Expand Down Expand Up @@ -63,17 +75,26 @@ describe("ExternalToolSection", () => {
};
};

beforeEach(() => {
useSchoolExternalToolUsageMock =
createMock<ReturnType<typeof useSchoolExternalToolUsage>>();

jest
.mocked(useSchoolExternalToolUsage)
.mockReturnValue(useSchoolExternalToolUsageMock);
});

describe("when component is used", () => {
it("should be found in the dom", () => {
const { wrapper } = setup();
const { wrapper } = getWrapper();
expect(wrapper.findComponent(ExternalToolSection).exists()).toBeTruthy();
});
});

describe("onMounted is called", () => {
describe("when component is mounted", () => {
it("should load the external tools", () => {
const { schoolExternalToolsModule } = setup();
const { schoolExternalToolsModule } = getWrapper();

expect(
schoolExternalToolsModule.loadSchoolExternalTools
Expand All @@ -86,7 +107,7 @@ describe("ExternalToolSection", () => {
const setupItems = () => {
const firstToolName = "Test";
const secondToolName = "Test2";
const { wrapper, schoolExternalToolsModule } = setup({
const { wrapper, schoolExternalToolsModule } = getWrapper({
getSchoolExternalTools: [
{
id: "testId",
Expand Down Expand Up @@ -213,7 +234,7 @@ describe("ExternalToolSection", () => {

describe("when deletion is confirmed", () => {
it("should call externalToolsModule.deleteSchoolExternalTool", async () => {
const { wrapper, schoolExternalToolsModule } = setup({
const { wrapper, schoolExternalToolsModule } = getWrapper({
getSchoolExternalTools: [
{
id: "testId",
Expand All @@ -234,7 +255,9 @@ describe("ExternalToolSection", () => {
const deleteButton = firstRowButtons.at(1);
await deleteButton.trigger("click");

const confirmButton = wrapper.find("[data-testId=dialog-confirm]");
const confirmButton = wrapper.find(
"[data-testId=delete-dialog-confirm]"
);
await confirmButton.trigger("click");

expect(
Expand All @@ -243,7 +266,7 @@ describe("ExternalToolSection", () => {
});

it("should call notifierModule.show", async () => {
const { wrapper, notifierModule } = setup({
const { wrapper, notifierModule } = getWrapper({
getSchoolExternalTools: [
{
id: "testId",
Expand All @@ -264,7 +287,9 @@ describe("ExternalToolSection", () => {
const deleteButton = firstRowButtons.at(1);
await deleteButton.trigger("click");

const confirmButton = wrapper.find("[data-testId=dialog-confirm]");
const confirmButton = wrapper.find(
"[data-testId=delete-dialog-confirm]"
);
await confirmButton.trigger("click");

expect(notifierModule.show).toHaveBeenCalled();
Expand All @@ -277,7 +302,7 @@ describe("ExternalToolSection", () => {
describe("getItemName is called", () => {
describe("when itemToDelete is set", () => {
it("should return the name", () => {
const { wrapper } = setup();
const { wrapper } = getWrapper();

const expectedName = "Name";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
Expand All @@ -298,7 +323,7 @@ describe("ExternalToolSection", () => {

describe("when itemToDelete is not set", () => {
it("should return an empty string", () => {
const { wrapper } = setup();
const { wrapper } = getWrapper();

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
Expand All @@ -312,4 +337,100 @@ describe("ExternalToolSection", () => {
});
});
});

describe("when deleting a schoolExternalTool", () => {
describe("when metadata is given", () => {
const setup = () => {
const { wrapper } = getWrapper({
getSchoolExternalTools: [
schoolExternalToolFactory.build({}),
schoolExternalToolFactory.build(),
],
});

const schoolExternalToolMetadata =
schoolExternalToolMetadataFactory.build();

useSchoolExternalToolUsageMock.metadata = ref(
schoolExternalToolMetadata
);

return {
wrapper,
};
};

it("should display tool usage count", async () => {
const { wrapper } = setup();

const deletebtn = wrapper.find('[data-testId="deleteAction"]');
await deletebtn.trigger("click");

const toolUsageCount = wrapper.find(
'[data-testid="delete-dialog-content"]'
);

expect(toolUsageCount).not.toBe("visible");
});

it("should display notification", async () => {
const { wrapper } = setup();

const deletebtn = wrapper.find('[data-testId="deleteAction"]');
await deletebtn.trigger("click");

const toolUsageCount = wrapper.find(
'[data-testid="delete-dialog-content"]'
);

expect(toolUsageCount).not.toBe("visible");
});
});

describe("when metadata is undefined", () => {
const setup = () => {
const { wrapper, notifierModule } = getWrapper({
getSchoolExternalTools: [
schoolExternalToolFactory.build({}),
schoolExternalToolFactory.build(),
],
});

useSchoolExternalToolUsageMock.metadata = ref(undefined);

return {
wrapper,
notifierModule,
};
};

it("should not display delete dialog", async () => {
const { wrapper } = setup();

const tableRows = wrapper.find("tbody").findAll("tr");

const firstRowButtons = tableRows.at(0).findAll("button");

const deleteButton = firstRowButtons.at(1);
await deleteButton.trigger("click");

const toolUsageCount = wrapper.find('[data-testid="delete-dialog"]');

expect(toolUsageCount).not.toBe("visible");
});

it("should display notification", async () => {
const { wrapper, notifierModule } = setup();

const deletebtn = wrapper.find('[data-testId="deleteAction"]');
await deletebtn.trigger("click");

expect(notifierModule.show).toHaveBeenCalled();
mrikallab marked this conversation as resolved.
Show resolved Hide resolved
expect(notifierModule.show).toHaveBeenCalledWith({
status: "error",
text: "components.administration.externalToolsSection.dialog.content.metadata.error",
});
});
});
});
});
36 changes: 30 additions & 6 deletions src/components/administration/ExternalToolSection.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
{{ t("components.administration.externalToolsSection.info") }}
</p>
<v-data-table
data-testid="external-tool-section-table"
v-if="items.length"
:disable-pagination="true"
:hide-default-footer="true"
Expand Down Expand Up @@ -45,9 +46,14 @@
{{ t("components.administration.externalToolsSection.action.add") }}
</v-btn>

<v-dialog v-model="isDeleteDialogOpen" max-width="360">
<v-dialog
v-if="metadata"
data-testid="delete-dialog"
v-model="isDeleteDialogOpen"
max-width="360"
>
<v-card :ripple="false">
<v-card-title>
<v-card-title data-testid="delete-dialog-title">
<h2 class="text-h4 my-2">
{{
t("components.administration.externalToolsSection.dialog.title")
Expand All @@ -56,11 +62,16 @@
</v-card-title>
<v-card-text class="text--primary">
<RenderHTML
data-testid="delete-dialog-content"
class="text-md mt-2"
:html="
t(
'components.administration.externalToolsSection.dialog.content',
{ itemName: getItemName }
{
itemName: getItemName,
courseCount: metadata.course,
boardElementCount: metadata.boardElement,
}
)
"
component="p"
Expand All @@ -69,7 +80,7 @@
<v-card-actions>
<v-spacer />
<v-btn
data-testId="dialog-cancel"
data-testId="delete-dialog-cancel"
class="dialog-closed"
depressed
text
Expand All @@ -78,7 +89,7 @@
{{ t("common.actions.cancel") }}
</v-btn>
<v-btn
data-testId="dialog-confirm"
data-testId="delete-dialog-confirm"
class="dialog-confirmed px-6"
color="primary"
depressed
Expand Down Expand Up @@ -120,6 +131,7 @@ import { DataTableHeader } from "vuetify";
import { useExternalToolsSectionUtils } from "./external-tool-section-utils.composable";
import ExternalToolToolbar from "./ExternalToolToolbar.vue";
import { SchoolExternalToolItem } from "./school-external-tool-item";
import { useSchoolExternalToolUsage } from "@data-external-tool";

export default defineComponent({
name: "ExternalToolSection",
Expand Down Expand Up @@ -147,6 +159,8 @@ export default defineComponent({
i18n.tc(key, 0, values);

const { getHeaders, getItems } = useExternalToolsSectionUtils(t);
const { fetchSchoolExternalToolUsage, metadata } =
useSchoolExternalToolUsage();

const headers: DataTableHeader[] = getHeaders;

Expand Down Expand Up @@ -189,9 +203,18 @@ export default defineComponent({

const isDeleteDialogOpen: Ref<boolean> = ref(false);

const openDeleteDialog = (item: SchoolExternalToolItem) => {
const openDeleteDialog = async (item: SchoolExternalToolItem) => {
itemToDelete.value = item;
isDeleteDialogOpen.value = true;
await fetchSchoolExternalToolUsage(item.id);
if (!metadata.value) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not the best way to do it from a user experience standpoint, because when a user makes an action/presses a button, they expect some responding action to happen. If I remember correctly a delay of 500ms between action and response can already cause disorientation. So when the loading takes a second the button press does not feel responsive anymore.

Copy link
Contributor

@arnegns arnegns Nov 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will be fixed with other dialog requirements like error handling in the next ticket

notifierModule.show({
text: t(
"components.administration.externalToolsSection.dialog.content.metadata.error"
),
status: "error",
});
}
};

const onCloseDeleteDialog = () => {
Expand All @@ -214,6 +237,7 @@ export default defineComponent({
getItemName,
mdiRefreshCircle,
mdiCheckCircle,
metadata,
};
},
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { createMock, DeepMocked } from "@golevelup/ts-jest";
import * as serverApi from "../../serverApi/v3/api";
import { SchoolExternalToolMetadataResponse } from "../../serverApi/v3/api";
import { mockApiResponse } from "../../../tests/test-utils";
import { useExternalToolApi } from "./ExternalToolApi.composable";
import {
SchoolExternalToolMetadata,
} from "../../store/external-tool";
import { schoolExternalToolMetadataResponseFactory } from "../../../tests/test-utils/factory/schoolExternalToolMetadataResponseFactory";
mrikallab marked this conversation as resolved.
Show resolved Hide resolved
import { useSchoolExternalToolApi } from "./SchoolExternalToolApi.composable";

describe("SchoolExternalToolApi.composable", () => {
let toolApi: DeepMocked<serverApi.ToolApiInterface>;

beforeEach(() => {
toolApi = createMock<serverApi.ToolApiInterface>();

jest.spyOn(serverApi, "ToolApiFactory").mockReturnValue(toolApi);
});

afterEach(() => {
jest.clearAllMocks();
});

describe("fetchSchoolExternalToolMetadata", () => {
const setup = () => {
const request: SchoolExternalToolMetadataResponse =
schoolExternalToolMetadataResponseFactory.build();

toolApi.toolSchoolControllerGetMetaDataForExternalTool.mockResolvedValue(
mockApiResponse({ data: request })
);

return {
request,
};
};

it("should call the api for metadata of schoolExternalTool", async () => {
setup();

await useExternalToolApi().fetchLaunchDataCall("contextExternalToolId");

expect(
toolApi.toolLaunchControllerGetToolLaunchRequest
).toHaveBeenCalledWith("contextExternalToolId");
});

it("should return metadata", async () => {
const { request } = setup();

const result: SchoolExternalToolMetadata =
await useSchoolExternalToolApi().fetchSchoolExternalToolMetadata(
"schoolExternalToolId"
);

expect(result).toEqual<SchoolExternalToolMetadata>({
course: 5,
boardElement: 6,
});
});
});
});
Loading
Loading