diff --git a/.env.example b/.env.example index 3b3995570..86969f78c 100644 --- a/.env.example +++ b/.env.example @@ -20,3 +20,6 @@ JWT_SECRET= #Crowdin CROWDIN_PERSONAL_TOKEN= CROWDIN_PROJECT_ID= + +# For dmptool-narrative-generator service +NARRATIVE_SERVICE_URL=http://narrative:3030 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index e73498b97..8ea4eb811 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,50 @@ ### Added +- Added missing `slug` property to all `tags` in the graphQL queries +- Added guidance text from the backend to the Question Answer page [#580] +- Added new, shared `ProjectRoles` component that generates the list of Project Roles for both the Project Member Search and Edit Project member pages [#945] +- Added `Help text` fields to `Description, Repositories, Metadata Standards, License, Access Level and Custom Text` fields in Research Output question type [#970] +- Added customizable `Initial Access Level` field to the QuestionAdd page for Research Outputs question type [#969] +- Added new `Start DMP` page at `projects/[projectId]/dmp/start` to direct user to create new plan or upload existing [#956] +- Added `autosave` back to the `PlanOverviewQuestionpage` [#944] +- Hooked up the `Download plan` page and added a `download-narrative` api endpoint [#313] + +### Updated +- Guidance page updates [#934] + - Hooked up `admin/guidance` page that lists all the `guidance groups` and offers `Create Guidance Group` button and edit options for existing Guidance Groups [#934] + - Hooked up `admin/guidance/groups/create`, and created a new page for `admin/guidance/groups/[groupId]` to include guidance text for all tags in one place and to allow + publishing at that level [#934] + - Added skeleton for loading of TinyMCEEditor. Especially needed it for the new Guidance Group Edit page, since we are loading so many at once [#934] + - Updated stripHtmlTags to include replacement of ` ` [#934] + - Updated `DashboardListItem` to have the option to be fully clickable. This makes clicking on smaller devices easier [#934] +- Made improvements to auth handling in `middleware` and `authHelper.ts` [#1035] +- Made text changes to upcoming blog posts [#989] +- For Research Outputs, updated repositories and metadata standards fields to be automatically enabled when user selects custom ones [#943] +- Updated Licenses and Output Types to use label "Use custom list" [#943] +- Updated `Output Types` in static `Research Outputs` table [#962] + - Added a description field to custom types + - Added tooltip icons next to each default output type with the description + - Updated Output Types component to behave like the Licenses component, where users can delete default types + - Updated unit test +- Improved project creation steps by updating pages in the flow (header, home, plan dashboard, create project details,and funding search) to help users in creating a plan [#956] +- Updated Project Details subdomains field to only display once a user selects a research domain [#947] +- Updated `Remove` buttons to be `secondary` buttons, rather than `red` [#964] + +### Fixed +- For Research Outputs, fixed custom Licenses select field to display selected value [#943] +- Fixed issue where custom repos and standards were not saving in state [#943] + +### Removed +- Removed Research Outputs section from the plan overview page +- Removed reference to the old `outputs` association on the graphQL query +- Removed `Description` fields for Research Output question fields [#970] +- Removed `scrollToTop` from `Template Create` page [#950] + +### Chore +- Updated `next` version to `15.5.7` due to vulnerability +- Ran `npm audit fix` to address `glob` vulnerability and `js-yaml` vulnerability +==================================================================================================================================== +## All changes above the line happened after the merge to the main branch on Nov 3, 2025 +### Added - Added user's org as a filter for the Plan Create (`projects/9/dmp/create`) page, and updated filter text to include `organization` [#735] - Fixed filtering on the Plan Create (`projects/9/dmp/create`) page so that it takes search term into consideration when used with checked filters [#735] - Moved checkbox filters below search field to make them more noticeable [#735] @@ -6,7 +52,7 @@ - Removed `Title` field from TinyMCEEditor's `Insert Link` form, and made `new window` the default for `Open link in...` [#874] - Integrated related works UI with GraphQL backend. - Added updated description that includes template source, affiliation name, version and publish date on the `Plan Overview` page [#621] - +- Added plan feedback status (Feedback received, Feedback requested, No feedback) to the `Plan Overview` page [#411] ### Updated - Updated `TinyMCEEditor` to allow users to change text color and background color. diff --git a/app/[locale]/__tests__/page.spec.tsx b/app/[locale]/__tests__/page.spec.tsx index 845303667..e8aca56ce 100644 --- a/app/[locale]/__tests__/page.spec.tsx +++ b/app/[locale]/__tests__/page.spec.tsx @@ -10,37 +10,33 @@ describe('Home Page', () => { render(); // Check for the heading - expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('Home Page'); + expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('title'); // Check for the PageLinkCard sections - expect(screen.getByText('Create & Manage')).toBeInTheDocument(); - expect(screen.getByText('Account & Administration')).toBeInTheDocument(); + expect(screen.getByText('createAndManage.title')).toBeInTheDocument(); + expect(screen.getByText('createAndManage.description')).toBeInTheDocument(); + expect(screen.getByText('accountAndAdmin.title')).toBeInTheDocument(); + expect(screen.getByText('accountAndAdmin.description')).toBeInTheDocument(); // Check for section descriptions - expect(screen.getByText('Create new templates and projects')).toBeInTheDocument(); - expect(screen.getByText('Manage your account and access admin features')).toBeInTheDocument(); + expect(screen.getByText('sections.templateManagement.title')).toBeInTheDocument(); + expect(screen.getByText('sections.templateManagement.description')).toBeInTheDocument(); + expect(screen.getByText('sections.planDashboard.title')).toBeInTheDocument(); + expect(screen.getByText('sections.planDashboard.description')).toBeInTheDocument(); + expect(screen.getByText('sections.accountSettings.title')).toBeInTheDocument(); + expect(screen.getByText('sections.accountSettings.description')).toBeInTheDocument(); + expect(screen.getByText('sections.adminOverview.title')).toBeInTheDocument(); + expect(screen.getByText('sections.adminOverview.description')).toBeInTheDocument(); // Check for all links to be present const allLinks = screen.getAllByRole('link'); expect(allLinks).toHaveLength(4); - - expect(screen.getByText('Template Management')).toBeInTheDocument(); - expect(screen.getByText('Project Management')).toBeInTheDocument(); - expect(screen.getByText('Account Settings')).toBeInTheDocument(); - expect(screen.getByText('Admin Overview')).toBeInTheDocument(); - - expect(screen.getByText('Create and manage templates (Must be Admin to access)')).toBeInTheDocument(); - expect(screen.getByText('Create and manage projects')).toBeInTheDocument(); - expect(screen.getByText('View and manage your account')).toBeInTheDocument(); - expect(screen.getByText('Access administrative functions')).toBeInTheDocument(); - - - const templateManagementLink = screen.getByText('Template Management').closest('a'); - const projectManagementLink = screen.getByText('Project Management').closest('a'); - const accountSettingsLink = screen.getByText('Account Settings').closest('a'); - const adminOverviewLink = screen.getByText('Admin Overview').closest('a'); + const templateManagementLink = screen.getByText('sections.templateManagement.title').closest('a'); + const projectManagementLink = screen.getByText('sections.planDashboard.title').closest('a'); + const accountSettingsLink = screen.getByText('sections.accountSettings.title').closest('a'); + const adminOverviewLink = screen.getByText('sections.adminOverview.title').closest('a'); expect(templateManagementLink).toHaveAttribute('href', '/en-US/template'); expect(projectManagementLink).toHaveAttribute('href', '/en-US/projects'); diff --git a/app/[locale]/admin/guidance/__mocks__/mockGuidanceGroupsData.json b/app/[locale]/admin/guidance/__mocks__/mockGuidanceGroupsData.json new file mode 100644 index 000000000..ad38ab9f5 --- /dev/null +++ b/app/[locale]/admin/guidance/__mocks__/mockGuidanceGroupsData.json @@ -0,0 +1,256 @@ +{ + "guidanceGroups": [ + { + "__typename": "GuidanceGroup", + "id": 2397, + "name": "CDL Guidance Group", + "description": "This is the guidance group for all CDL guidance", + "latestPublishedVersion": "1", + "latestPublishedDate": "2025-11-26 19:43:42", + "guidance": [ + { + "__typename": "Guidance", + "tagId": 2, + "guidanceText": "

This is guidance text for Preservation

", + "id": 974 + }, + { + "__typename": "Guidance", + "tagId": 3, + "guidanceText": "

This is guidance text for Data Collection

", + "id": 975 + }, + { + "__typename": "Guidance", + "tagId": 11, + "guidanceText": "

This is guidance text for Data sharing

", + "id": 976 + }, + { + "__typename": "Guidance", + "tagId": 1, + "guidanceText": "

Guidance text for storage & security123

", + "id": 977 + }, + { + "__typename": "Guidance", + "tagId": 4, + "guidanceText": "

Guidance text for data format

", + "id": 978 + }, + { + "__typename": "Guidance", + "tagId": 5, + "guidanceText": "

Guidance text for roles & responsibilities

", + "id": 979 + }, + { + "__typename": "Guidance", + "tagId": 6, + "guidanceText": "

Guidance for Ethics & privacy

", + "id": 980 + }, + { + "__typename": "Guidance", + "tagId": 7, + "guidanceText": "

Guidance text for intellectual propert rights

", + "id": 981 + }, + { + "__typename": "Guidance", + "tagId": 8, + "guidanceText": "

Guidance text for related policies123

", + "id": 982 + }, + { + "__typename": "Guidance", + "tagId": 9, + "guidanceText": "

Adding guidance text for budget, which didn't exist before

", + "id": 983 + } + ], + "modifiedBy": { + "__typename": "User", + "givenName": "Super", + "surName": "Admin", + "id": 1 + }, + "modified": "2025-11-25 23:38:35", + "isDirty": true, + "versionedGuidanceGroup": [ + { + "__typename": "VersionedGuidanceGroup", + "active": false, + "id": 1001, + "version": 1 + } + ] + }, + { + "__typename": "GuidanceGroup", + "id": 2396, + "name": "Data sharing", + "description": "", + "latestPublishedVersion": null, + "latestPublishedDate": null, + "guidance": [], + "modifiedBy": { + "__typename": "User", + "givenName": "Test", + "surName": "Admin", + "id": 2 + }, + "modified": "2025-11-25 00:00:00", + "isDirty": true, + "versionedGuidanceGroup": [] + }, + { + "__typename": "GuidanceGroup", + "id": 210, + "name": "DMPTool", + "description": "", + "latestPublishedVersion": "1", + "latestPublishedDate": "2017-12-04 14:30:41", + "guidance": [ + { + "__typename": "Guidance", + "tagId": 1, + "guidanceText": "", + "id": 960 + }, + { + "__typename": "Guidance", + "tagId": 2, + "guidanceText": "", + "id": 961 + }, + { + "__typename": "Guidance", + "tagId": 3, + "guidanceText": "", + "id": 962 + }, + { + "__typename": "Guidance", + "tagId": 14, + "guidanceText": "", + "id": 963 + }, + { + "__typename": "Guidance", + "tagId": 4, + "guidanceText": "", + "id": 964 + }, + { + "__typename": "Guidance", + "tagId": 5, + "guidanceText": "", + "id": 965 + }, + { + "__typename": "Guidance", + "tagId": 6, + "guidanceText": "", + "id": 966 + }, + { + "__typename": "Guidance", + "tagId": 7, + "guidanceText": "", + "id": 967 + }, + { + "__typename": "Guidance", + "tagId": 8, + "guidanceText": "", + "id": 968 + }, + { + "__typename": "Guidance", + "tagId": 9, + "guidanceText": "", + "id": 969 + }, + { + "__typename": "Guidance", + "tagId": 10, + "guidanceText": "", + "id": 970 + }, + { + "__typename": "Guidance", + "tagId": 11, + "guidanceText": "", + "id": 971 + }, + { + "__typename": "Guidance", + "tagId": 12, + "guidanceText": "", + "id": 972 + }, + { + "__typename": "Guidance", + "tagId": 13, + "guidanceText": "", + "id": 973 + } + ], + "modifiedBy": { + "__typename": "User", + "givenName": "Test", + "surName": "Admin", + "id": 2 + }, + "modified": "2025-11-25 00:00:00", + "isDirty": false, + "versionedGuidanceGroup": [ + { + "__typename": "VersionedGuidanceGroup", + "active": true, + "id": 1002, + "version": 1 + } + ] + }, + { + "__typename": "GuidanceGroup", + "id": 2398, + "name": "Test Guidance Group", + "description": "This is the description for my new Guidance Group123", + "latestPublishedVersion": "1", + "latestPublishedDate": "2025-11-30 19:21:28", + "guidance": [ + { + "__typename": "Guidance", + "tagId": 1, + "guidanceText": "

This is the storage & security guidance

", + "id": 984 + }, + { + "__typename": "Guidance", + "tagId": 2, + "guidanceText": "

This is the preservation guidance

", + "id": 985 + } + ], + "modifiedBy": { + "__typename": "User", + "givenName": "Super", + "surName": "Admin", + "id": 1 + }, + "modified": "2025-11-30 21:05:24", + "isDirty": true, + "versionedGuidanceGroup": [ + { + "__typename": "VersionedGuidanceGroup", + "active": false, + "id": 1003, + "version": 1 + } + ] + } + ] +} \ No newline at end of file diff --git a/app/[locale]/admin/guidance/__mocks__/mockMeData.json b/app/[locale]/admin/guidance/__mocks__/mockMeData.json new file mode 100644 index 000000000..b7df9ae71 --- /dev/null +++ b/app/[locale]/admin/guidance/__mocks__/mockMeData.json @@ -0,0 +1,33 @@ +{ + "me": { + "__typename": "User", + "id": 1, + "givenName": "Super", + "surName": "Admin", + "languageId": "en-US", + "role": "SUPERADMIN", + "emails": [ + { + "__typename": "UserEmail", + "id": 1, + "email": "super@example.com", + "isPrimary": true, + "isConfirmed": true + } + ], + "errors": { + "__typename": "UserErrors", + "general": null, + "email": null, + "password": null, + "role": null + }, + "affiliation": { + "__typename": "Affiliation", + "id": 1, + "name": "California Digital Library", + "searchName": "California Digital Library | cdlib.org | CDL ", + "uri": "https://ror.org/03yrm5c12" + } + } +} \ No newline at end of file diff --git a/app/[locale]/admin/guidance/__mocks__/mockTagsData.json b/app/[locale]/admin/guidance/__mocks__/mockTagsData.json new file mode 100644 index 000000000..434dbb85d --- /dev/null +++ b/app/[locale]/admin/guidance/__mocks__/mockTagsData.json @@ -0,0 +1,102 @@ +{ + "tags": [ + { + "__typename": "Tag", + "id": 1, + "name": "Storage & security", + "slug": "storage-security", + "description": "" + }, + { + "__typename": "Tag", + "id": 2, + "name": "Preservation", + "slug": "preservation", + "description": "" + }, + { + "__typename": "Tag", + "id": 3, + "name": "Data collection", + "slug": "data-collection", + "description": "" + }, + { + "__typename": "Tag", + "id": 4, + "name": "Data format", + "slug": "data-format", + "description": "" + }, + { + "__typename": "Tag", + "id": 5, + "name": "Roles & responsibilities", + "slug": "roles-responsibilities", + "description": "" + }, + { + "__typename": "Tag", + "id": 6, + "name": "Ethics & privacy", + "slug": "ethics-privacy", + "description": "" + }, + { + "__typename": "Tag", + "id": 7, + "name": "Intellectual property rights", + "slug": "intellectual-property-rights", + "description": "" + }, + { + "__typename": "Tag", + "id": 8, + "name": "Related policies", + "slug": "related-policies", + "description": "" + }, + { + "__typename": "Tag", + "id": 9, + "name": "Budget", + "slug": "budget", + "description": "" + }, + { + "__typename": "Tag", + "id": 10, + "name": "Data repository", + "slug": "data-repository", + "description": "" + }, + { + "__typename": "Tag", + "id": 11, + "name": "Data sharing", + "slug": "data-sharing", + "description": "" + }, + { + "__typename": "Tag", + "id": 12, + "name": "Metadata & documentation", + "slug": "metadata-documentation", + "description": "" + }, + { + "__typename": "Tag", + "id": 13, + "name": "Data volume", + "slug": "data-volume", + "description": "" + }, + { + "__typename": "Tag", + "id": 14, + "name": "Data description", + "slug": "data-description", + "description": "" + } + ] +} \ No newline at end of file diff --git a/app/[locale]/admin/guidance/__tests__/page.spec.tsx b/app/[locale]/admin/guidance/__tests__/page.spec.tsx index 25cc45e1e..7914f717d 100644 --- a/app/[locale]/admin/guidance/__tests__/page.spec.tsx +++ b/app/[locale]/admin/guidance/__tests__/page.spec.tsx @@ -1,10 +1,26 @@ import React from "react"; import { render, screen, waitFor } from "@testing-library/react"; import "@testing-library/jest-dom"; -import { axe, toHaveNoViolations } from "jest-axe"; +import { MockedProvider } from "@apollo/client/testing"; +import { useFormatter, useTranslations } from "next-intl"; +import { + MeDocument, + TagsDocument, + GuidanceGroupsDocument +} from "@/generated/graphql"; +import { axe, toHaveNoViolations } from "jest-axe"; +import mockMeData from "../__mocks__/mockMeData.json"; +import mockTagsData from "../__mocks__/mockTagsData.json"; +import mockGuidanceGroupsData from "../__mocks__/mockGuidanceGroupsData.json"; import GuidancePage from "../page"; +// Mock next-intl hooks +jest.mock("next-intl", () => ({ + useFormatter: jest.fn(), + useTranslations: jest.fn(), +})); + // Mock Next.js navigation jest.mock("next/navigation", () => ({ useRouter: () => ({ @@ -17,84 +33,187 @@ jest.mock("next/navigation", () => ({ expect.extend(toHaveNoViolations); +const mocks = [ + { + request: { + query: MeDocument, + }, + result: { + data: mockMeData, + }, + }, + { + request: { + query: TagsDocument, + }, + result: { + data: mockTagsData, + }, + }, + { + request: { + query: GuidanceGroupsDocument, + variables: { + affiliationId: "https://ror.org/03yrm5c12", + } + }, + result: { + data: mockGuidanceGroupsData, + }, + }, +] describe("GuidancePage", () => { beforeEach(() => { window.scrollTo = jest.fn(); + (useFormatter as jest.Mock).mockReturnValue({ + dateTime: jest.fn((date) => date.toLocaleDateString()), + }); + + (useTranslations as jest.Mock).mockImplementation((namespace) => { + return (key: string) => `${namespace}.${key}`; + }); }); - afterEach(() => { - jest.clearAllMocks(); + it("should render the create group button", async () => { + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByText("Guidance.pages.index.createGroup")).toBeInTheDocument(); + }); }); - it("should render the create group button", () => { - render(); + it("should initially render loading message", () => { + render( + + + , + ); - expect(screen.getByText("pages.index.createGroup")).toBeInTheDocument(); + expect(screen.getByText("Loading...")).toBeInTheDocument(); }); - it("should render guidance groups list", () => { - render(); + it("should render guidance groups list", async () => { + render( + + + , + ); - const listContainer = screen.getByLabelText("Guidance groups list"); - expect(listContainer).toBeInTheDocument(); - expect(listContainer).toHaveAttribute("role", "list"); + await waitFor(() => { + const listContainer = screen.getByLabelText("Guidance groups list"); + expect(listContainer).toBeInTheDocument(); + expect(listContainer.tagName).toBe('UL'); + + }); }); - it("should render guidance group items", () => { - render(); + it("should render guidance group items", async () => { + render( + + + , + ); - const listItems = document.querySelectorAll('[role="listitem"], .guidance-content'); - expect(listItems.length).toBeGreaterThan(0); + await waitFor(() => { + const listItems = document.querySelectorAll('.guidanceContent'); + expect(listItems.length).toBeGreaterThan(0); + }) }); - it("should render guidance group metadata", () => { - render(); + it("should render guidance group metadata", async () => { + render( + + + , + ); - const lastRevisedElements = screen.getAllByText(/lastRevisedBy/); - const lastUpdatedElements = screen.getAllByText(/lastUpdated/); - expect(lastRevisedElements.length).toBeGreaterThan(0); - expect(lastUpdatedElements.length).toBeGreaterThan(0); + await waitFor(() => { + const lastUpdatedElement = screen.getAllByText(/Global\.lastUpdated\s*:\s*11-25-2025/i); + const statusElement = screen.getAllByText(/Guidance\.status\.status\s*:\s*Guidance.status.unpublishedChanges/i); + const guidanceTextCountElement = screen.getByText(/10\s*\/\s*14\s*Tags with Guidance/i); + expect(lastUpdatedElement.length).toBeGreaterThan(0); + expect(statusElement.length).toBeGreaterThan(0); + expect(guidanceTextCountElement).toBeInTheDocument(); + }); }); - it("should render guidance group descriptions", () => { - render(); + it("should render guidance group descriptions", async () => { + render( + + + , + ); - const descriptions = document.querySelectorAll(".description"); - expect(descriptions.length).toBeGreaterThan(0); + await waitFor(() => { + const descriptions = document.querySelectorAll(".description"); + expect(descriptions.length).toBeGreaterThan(0); + }); }); - it("should render guidance group status information", () => { - render(); + it("should render guidance group status information", async () => { + render( + + + , + ); - const publishedElements = screen.getAllByText(/status\.status\s*:\s*Published/); - const draftElements = screen.getAllByText(/status\.status\s*:\s*Draft/); - expect(publishedElements.length).toBeGreaterThan(0); - expect(draftElements.length).toBeGreaterThan(0); + await waitFor(() => { + const publishedElements = screen.getAllByText(/Guidance\.status\.status\s*:\s*Guidance.status.published/); + const draftElements = screen.getAllByText(/Guidance\.status\.status\s*:\s*Guidance.status.draft/); + expect(publishedElements.length).toBeGreaterThan(0); + expect(draftElements.length).toBeGreaterThan(0); + }); }); - it("should render multiple guidance groups", () => { - render(); + it("should render multiple guidance groups", async () => { + render( + + + , + ); - const guidanceItems = document.querySelectorAll(".guidance-list > *"); - expect(guidanceItems.length).toBeGreaterThan(1); + await waitFor(() => { + const guidanceItems = document.querySelectorAll(".guidanceList > *"); + expect(guidanceItems.length).toBeGreaterThan(1); + }) }); - it("should have guidance-specific structure", () => { - render(); + it("should have guidance-specific structure", async () => { + render( + + + , + ); - const guidanceList = document.querySelector(".guidance-list"); - expect(guidanceList).toBeInTheDocument(); + await waitFor(() => { + const guidanceList = document.querySelector(".guidanceList"); + expect(guidanceList).toBeInTheDocument(); + }); }); - it("should render author information in metadata", () => { - render(); + it("should render author information in metadata", async () => { + render( + + + , + ); - const metadata = document.querySelectorAll(".metadata"); - expect(metadata.length).toBeGreaterThan(0); + await waitFor(() => { + const metadata = document.querySelectorAll(".metadata"); + expect(metadata.length).toBeGreaterThan(0); + }); }); it("should pass accessibility tests", async () => { - const { container } = render(); + const { container } = render( + + + , + ); await waitFor(() => { expect(screen.getByLabelText("Guidance groups list")).toBeInTheDocument(); diff --git a/app/[locale]/admin/guidance/groups/[groupId]/__mocks__/mockGuidanceByGroupData.json b/app/[locale]/admin/guidance/groups/[groupId]/__mocks__/mockGuidanceByGroupData.json new file mode 100644 index 000000000..e3d03f102 --- /dev/null +++ b/app/[locale]/admin/guidance/groups/[groupId]/__mocks__/mockGuidanceByGroupData.json @@ -0,0 +1,44 @@ +{ + "guidanceByGroup": [ + { + "__typename": "Guidance", + "modifiedBy": { + "__typename": "User", + "givenName": "Super", + "surName": "Admin", + "id": 1 + }, + "guidanceText": "

This is the guidance for storage & security

", + "id": 976, + "tagId": 1, + "errors": { + "__typename": "GuidanceErrors", + "general": null, + "tagId": null, + "guidanceText": null, + "guidanceGroupId": null + }, + "modified": "2025-12-01 19:58:23" + }, + { + "__typename": "Guidance", + "modifiedBy": { + "__typename": "User", + "givenName": "Super", + "surName": "Admin", + "id": 1 + }, + "guidanceText": "

This is the guidance for Preservation

", + "id": 977, + "tagId": 2, + "errors": { + "__typename": "GuidanceErrors", + "general": null, + "tagId": null, + "guidanceText": null, + "guidanceGroupId": null + }, + "modified": "2025-12-01 20:00:36" + } + ] +} \ No newline at end of file diff --git a/app/[locale]/admin/guidance/groups/[groupId]/__mocks__/mockGuidanceGroupData.json b/app/[locale]/admin/guidance/groups/[groupId]/__mocks__/mockGuidanceGroupData.json new file mode 100644 index 000000000..723e88d83 --- /dev/null +++ b/app/[locale]/admin/guidance/groups/[groupId]/__mocks__/mockGuidanceGroupData.json @@ -0,0 +1,41 @@ +{ + "guidanceGroup": { + "__typename": "GuidanceGroup", + "id": 2398, + "name": "CDL Guidance Group", + "description": "The guidance group for CDL", + "bestPractice": false, + "latestPublishedVersion": "2", + "latestPublishedDate": "2025-12-01 20:00:45", + "isDirty": false, + "guidance": [ + { + "__typename": "Guidance", + "guidanceText": "

This is the guidance for storage & security

", + "id": 976, + "tagId": 1 + }, + { + "__typename": "Guidance", + "guidanceText": "

This is the guidance for Preservation

", + "id": 977, + "tagId": 2 + } + ], + "versionedGuidanceGroup": [ + { + "__typename": "VersionedGuidanceGroup", + "active": true, + "id": 213, + "version": 2 + }, + { + "__typename": "VersionedGuidanceGroup", + "active": false, + "id": 212, + "version": 1 + } + ], + "optionalSubset": false + } +} \ No newline at end of file diff --git a/app/[locale]/admin/guidance/groups/[groupId]/__tests__/page.spec.tsx b/app/[locale]/admin/guidance/groups/[groupId]/__tests__/page.spec.tsx index 5bb62c461..a17ee21c5 100644 --- a/app/[locale]/admin/guidance/groups/[groupId]/__tests__/page.spec.tsx +++ b/app/[locale]/admin/guidance/groups/[groupId]/__tests__/page.spec.tsx @@ -1,104 +1,907 @@ import React from "react"; -import { render, screen, waitFor } from "@testing-library/react"; +import { + fireEvent, + render, + screen, + waitFor, + waitForElementToBeRemoved, + within +} from "@testing-library/react"; import "@testing-library/jest-dom"; -import { axe, toHaveNoViolations } from "jest-axe"; +import { MockedProvider } from "@apollo/client/testing"; +import { useFormatter, useTranslations } from "next-intl"; +import { useParams, useRouter } from 'next/navigation'; +import logECS from '@/utils/clientLogger'; +import { + MeDocument, + TagsDocument, + GuidanceGroupDocument, + GuidanceByGroupDocument, +} from "@/generated/graphql"; + +import { + addGuidanceTextAction, + publishGuidanceGroupAction, + unPublishGuidanceGroupAction, + updateGuidanceAction +} from '../actions'; +import { axe, toHaveNoViolations } from "jest-axe"; +import { useToast } from '@/context/ToastContext'; +import { mockScrollIntoView } from '@/__mocks__/common'; +import mockMeData from "../../../__mocks__/mockMeData.json"; +import mockTagsData from "../../../__mocks__/mockTagsData.json"; +import mockGuidanceByGroupData from "../__mocks__/mockGuidanceByGroupData.json"; +import mockGuidanceGroupData from "../__mocks__/mockGuidanceGroupData.json"; import GuidanceGroupIndexPage from "../page"; -// Mock Next.js navigation -jest.mock("next/navigation", () => ({ - useParams: () => ({ - groupId: "1", - }), - useRouter: () => ({ - back: jest.fn(), - push: jest.fn(), - replace: jest.fn(), - refresh: jest.fn(), - }), +// Mock TinyMCEEditor to simplify content updates in tests +jest.mock('@/components/TinyMCEEditor', () => { + const TinyMCEEditorMock = ({ content, setContent, id }: { content: string; setContent: (v: string) => void; id: string }) => ( +