From 68383cd74e407aad9369e19807d29769c894cf87 Mon Sep 17 00:00:00 2001 From: Toby Dixon Smith Date: Sat, 12 Oct 2024 03:43:17 +0100 Subject: [PATCH] fix: #405 , and both new and corrected tests for JsonGenerator (#406) * build: add @testing-library/dom to dependancies needed to run current tests * refactor: remove unnecessary named export in JsonGenerator side effect: improves code coverage * test: correct test case in CardForm tests * test: add test to cover final branch in CardForm * test: add tests to validate output of JsonPreview introduces a test which fails as it correctly identifies an issue with the current code * fix: compare schema value types in isDataCached to invalidate cache correctly Modified isDataCached to compare both keys and value types in the schema to ensure cache invalidation when field types change. This resolves the issue where changing a field type (e.g., from int to firstName) did not update the preview as expected. * test: add test case to validate preview update after adding a field and setting its type in CardForm --- package-lock.json | 35 ++- package.json | 1 + .../JsonGenerator/components/CardForm.jsx | 28 +-- .../JsonGenerator/components/JsonPreview.jsx | 5 +- src/app/customizer/JsonGenerator/page.js | 2 +- .../JsonGenerator/tests/CardForm.test.js | 230 +++++++++++++++++- src/app/customizer/JsonGenerator/utils.js | 28 ++- 7 files changed, 284 insertions(+), 45 deletions(-) diff --git a/package-lock.json b/package-lock.json index b65ce5d8..ca21fd33 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,6 +63,7 @@ "uuid": "^10.0.0" }, "devDependencies": { + "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.5.0", "@testing-library/react": "^16.0.1", "eslint": "^8", @@ -3330,7 +3331,7 @@ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", "dev": true, - "peer": true, + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -3350,7 +3351,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "peer": true, + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -3366,7 +3367,7 @@ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, - "peer": true, + "license": "Apache-2.0", "dependencies": { "dequal": "^2.0.3" } @@ -3376,7 +3377,7 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, - "peer": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -3393,7 +3394,7 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, - "peer": true, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -3406,14 +3407,14 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, - "peer": true + "license": "MIT" }, "node_modules/@testing-library/dom/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, - "peer": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -3423,7 +3424,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, - "peer": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -3565,7 +3566,7 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "peer": true + "license": "MIT" }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -5841,7 +5842,7 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "peer": true + "license": "MIT" }, "node_modules/dom-helpers": { "version": "5.2.1", @@ -10844,7 +10845,7 @@ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, - "peer": true, + "license": "MIT", "bin": { "lz-string": "bin/bin.js" } @@ -11835,12 +11836,6 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/monaco-editor": { - "version": "0.50.0", - "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.50.0.tgz", - "integrity": "sha512-8CclLCmrRRh+sul7C08BmPBP3P8wVWfBHomsTcndxg5NRCEPfu/mc2AGU8k37ajjDVXcXFc12ORAMUkmk+lkFA==", - "peer": true - }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -12739,7 +12734,7 @@ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, - "peer": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -12754,7 +12749,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, - "peer": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -12767,7 +12762,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "peer": true + "license": "MIT" }, "node_modules/process-nextick-args": { "version": "2.0.1", diff --git a/package.json b/package.json index 59e5458c..b0f05a31 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "uuid": "^10.0.0" }, "devDependencies": { + "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.5.0", "@testing-library/react": "^16.0.1", "eslint": "^8", diff --git a/src/app/customizer/JsonGenerator/components/CardForm.jsx b/src/app/customizer/JsonGenerator/components/CardForm.jsx index d6826a48..99bbc6c7 100644 --- a/src/app/customizer/JsonGenerator/components/CardForm.jsx +++ b/src/app/customizer/JsonGenerator/components/CardForm.jsx @@ -8,6 +8,20 @@ import { ActionButtons } from "./ActionButtons"; import { PreviewSection } from "./PreviewSection"; import { initialFields } from "./Init"; +export const exportJsonData = (data) => { + const blob = new Blob( + [ + JSON.stringify( + data, + (_, value) => (typeof value === "bigint" ? value.toString() : value), + 2, + ), + ], + { type: "application/json" }, + ); + saveAs(blob, "WebDevTools.json"); +}; + export default function CardForm({ isDarkMode }) { const [fields, setFields] = useState(initialFields()); const [numRows, setNumRows] = useState(5); @@ -46,20 +60,6 @@ export default function CardForm({ isDarkMode }) { }).filter((item) => Object.keys(item).length > 0); }; - const exportJsonData = (data) => { - const blob = new Blob( - [ - JSON.stringify( - data, - (_, value) => (typeof value === "bigint" ? value.toString() : value), - 2, - ), - ], - { type: "application/json" }, - ); - saveAs(blob, "WebDevTools.json"); - }; - const resetClicks = () => { setIsLoading(false); setPreviewClicked(false); diff --git a/src/app/customizer/JsonGenerator/components/JsonPreview.jsx b/src/app/customizer/JsonGenerator/components/JsonPreview.jsx index 060d00e4..fe29931c 100644 --- a/src/app/customizer/JsonGenerator/components/JsonPreview.jsx +++ b/src/app/customizer/JsonGenerator/components/JsonPreview.jsx @@ -1,5 +1,8 @@ export const JsonPreview = ({ data }) => ( -
+  
     {JSON.stringify(
       data,
       (_, value) => (typeof value === "bigint" ? value.toString() : value),
diff --git a/src/app/customizer/JsonGenerator/page.js b/src/app/customizer/JsonGenerator/page.js
index 53312cfb..5cb71502 100644
--- a/src/app/customizer/JsonGenerator/page.js
+++ b/src/app/customizer/JsonGenerator/page.js
@@ -5,7 +5,7 @@ import CardForm from "./components/CardForm";
 import Heroish from "./components/Heroish";
 import { Nav } from "@/components/nav";
 
-export function JsonGeneratorMain() {
+function JsonGeneratorMain() {
   const [isDarkMode, setIsDarkMode] = useState(false);
 
   const toggleTheme = () => {
diff --git a/src/app/customizer/JsonGenerator/tests/CardForm.test.js b/src/app/customizer/JsonGenerator/tests/CardForm.test.js
index a19d7c7b..207b662b 100644
--- a/src/app/customizer/JsonGenerator/tests/CardForm.test.js
+++ b/src/app/customizer/JsonGenerator/tests/CardForm.test.js
@@ -1,6 +1,6 @@
 import React from "react";
 import { render, screen, fireEvent, waitFor } from "@testing-library/react";
-import CardForm from "../components/CardForm";
+import CardForm, { exportJsonData } from "../components/CardForm";
 import { saveAs } from "file-saver";
 
 // Mock external dependencies
@@ -74,10 +74,234 @@ describe("CardForm Component", () => {
   test('shows preview when "Preview" button is clicked', async () => {
     render();
     const fieldTypeSelects = screen.getAllByRole("combobox");
+    // Change all field types to "int" so preview button is enabled
     fieldTypeSelects.forEach((select) => {
       fireEvent.change(select, { target: { value: "int" } });
     });
-    const modalHeading = await screen.findByText(/preview/i);
-    expect(modalHeading).toBeInTheDocument();
+    // Click the preview button
+    const previewButton = await screen.getByRole("button", {
+      name: /preview/i,
+    });
+    fireEvent.click(previewButton);
+    // Check if the preview text is displayed
+    const previewSectionHeader = await screen.getByText("Preview", {
+      selector: "span:not(button span)",
+    });
+    expect(previewSectionHeader).toBeInTheDocument();
+  });
+
+  test("handles bigint values by converting them to strings", async () => {
+    // Arrange: Create a response data containing a bigint value
+    const responseData = [
+      {
+        id: "123",
+        largeNumber: BigInt(9007199254740991), // A sample bigint
+      },
+    ];
+
+    // Act: Call the exportJsonData function
+    exportJsonData(responseData);
+
+    // Extract the Blob passed to saveAs
+    const blob = saveAs.mock.calls[0][0];
+
+    // Use a FileReader to read the Blob content
+    const reader = new FileReader();
+    reader.readAsText(blob);
+
+    // Wait for the FileReader to load the contents
+    await new Promise((resolve) => {
+      reader.onloadend = resolve;
+    });
+
+    // Parse the Blob content as JSON
+    const jsonContent = JSON.parse(reader.result);
+
+    // Assert that the bigint was stringified correctly
+    expect(jsonContent[0].largeNumber).toBe("9007199254740991");
+    expect(saveAs).toHaveBeenCalledTimes(1);
+  });
+
+  test("generates correct output in the preview after changing field type", async () => {
+    // Arrange: Render the CardForm component
+    render();
+
+    // Find the dropdown for field types (assuming it's a select element)
+    const fieldTypeSelects = screen.getAllByRole("combobox");
+
+    // Set initial field type to "int"
+    fireEvent.change(fieldTypeSelects[0], { target: { value: "int" } });
+
+    // Now, change the field type to "firstName"
+    fireEvent.change(fieldTypeSelects[0], { target: { value: "firstName" } });
+
+    // Simulate clicking the preview button to show the JSON output on the screen
+    const previewButton = screen.getByRole("button", { name: /preview/i });
+    fireEvent.click(previewButton);
+
+    // Wait for the JSON preview to be rendered
+    await waitFor(() =>
+      screen.getByText("Preview", {
+        selector: "span:not(button span)",
+      }),
+    );
+
+    // Find the previewed JSON (assuming it appears in a 
 or 
tag) + const previewOutput = screen.getByTestId("preview-json"); // Adjust this based on your DOM structure + + // Parse the JSON output from the preview section + const jsonContent = JSON.parse(previewOutput.textContent); + + // Assert: Check that one of the fields in the generated JSON has a "firstName" value + const generatedFieldValues = Object.values(jsonContent[0]); + + // Check that one of the generated values is a string, since firstName should generate a string + const isFirstNameGenerated = generatedFieldValues.some( + (value) => typeof value === "string" && /^[A-Z][a-z]+$/.test(value), // Example regex for a name + ); + + expect(isFirstNameGenerated).toBe(true); // Expect that a firstName-like value was generated + }); + + test("updates the preview output after closing and changing field type", async () => { + // Arrange: Render the CardForm component + render(); + + // Find the dropdown for field types (assuming it's a select element) + const fieldTypeSelects = screen.getAllByRole("combobox"); + + // Set initial field type to "int" + fireEvent.change(fieldTypeSelects[0], { target: { value: "int" } }); + + // Simulate clicking the preview button to show the JSON output on the screen + const previewButton = screen.getByRole("button", { name: /preview/i }); + fireEvent.click(previewButton); + + // Wait for the preview to be rendered + await waitFor(() => + screen.getByText("Preview", { + selector: "span:not(button span)", + }), + ); + + // Find the previewed JSON (assuming it appears in a
 or 
tag) + const previewOutput = screen.getByTestId("preview-json"); // Adjust based on your DOM structure + + // Parse the JSON output from the preview section + const jsonContent = JSON.parse(previewOutput.textContent); + + // Assert: Check that the initial field type "int" generates a number + const generatedIntValue = Object.values(jsonContent[0]).some( + (value) => typeof value === "number", + ); + expect(generatedIntValue).toBe(true); + + // Close the preview by clicking the 'X' span (assuming the 'X' closes the preview) + const closeButton = screen.getByText("x"); // Adjust the selector if needed + fireEvent.click(closeButton); + + // Change the field type to "firstName" + fireEvent.change(fieldTypeSelects[0], { target: { value: "firstName" } }); + + // Simulate clicking the preview button again to show the updated JSON output + fireEvent.click(previewButton); + + // Wait for the preview to be rendered again + await waitFor(() => + screen.getByText("Preview", { + selector: "span:not(button span)", + }), + ); + + // Find the updated previewed JSON + const updatedPreviewOutput = screen.getByTestId("preview-json"); + + // Parse the updated JSON output + const updatedJsonContent = JSON.parse(updatedPreviewOutput.textContent); + + // Assert: Check that the new field type "firstName" generates a string + const generatedFirstNameValue = Object.values(updatedJsonContent[0]).some( + (value) => typeof value === "string" && /^[A-Z][a-z]+$/.test(value), // Example regex for a name + ); + expect(generatedFirstNameValue).toBe(true); + }); + + test("updates preview after adding a field and setting its type", async () => { + // Arrange: Render the CardForm component with 3 default fields and 5 entries in the preview + render(); + + // Ensure the initial fields have types (necessary for preview to be enabled) + const fieldTypeSelects = screen.getAllByRole("combobox"); + + // Set the initial types for the existing fields + fireEvent.change(fieldTypeSelects[0], { target: { value: "int" } }); + fireEvent.change(fieldTypeSelects[1], { target: { value: "firstName" } }); + fireEvent.change(fieldTypeSelects[2], { target: { value: "lastName" } }); + + // Simulate clicking the preview button to generate JSON with the initial fields + const previewButton = screen.getByRole("button", { name: /preview/i }); + fireEvent.click(previewButton); + + // Wait for the preview to be rendered + await waitFor(() => + screen.getByText("Preview", { + selector: "span:not(button span)", + }), + ); + + // Find the previewed JSON + const previewOutput = screen.getByTestId("preview-json"); + + // Parse the JSON output and check the number of entries (rows) + const jsonContent = JSON.parse(previewOutput.textContent); + expect(jsonContent.length).toBe(5); // Expect 5 entries (rows) + + // Check that each entry has 3 fields initially + jsonContent.forEach((entry) => { + expect(Object.keys(entry).length).toBe(3); // Each entry should have 3 fields + }); + + // Now, click the 'ADD ANOTHER FIELD' button to add a new field + const addFieldButton = screen.getByText(/add another field/i); + fireEvent.click(addFieldButton); + + // The preview should NOT change because the new field has no type yet + fireEvent.click(previewButton); + const unchangedPreviewOutput = screen.getByTestId("preview-json"); + const unchangedJsonContent = JSON.parse(unchangedPreviewOutput.textContent); + expect(unchangedJsonContent.length).toBe(5); // Still expect 5 entries + + // Each entry should still have 3 fields (since new field doesn't have a type yet) + unchangedJsonContent.forEach((entry) => { + expect(Object.keys(entry).length).toBe(3); // Fields count remains 3 + }); + + // Now, set the new field's type to "firstName" + const updatedFieldTypeSelects = screen.getAllByRole("combobox"); + fireEvent.change(updatedFieldTypeSelects[3], { + target: { value: "firstName" }, + }); // Select the new field type + + // Simulate clicking the preview button again after setting the type + fireEvent.click(previewButton); + + // Wait for the preview to be rendered again + await waitFor(() => + screen.getByText("Preview", { + selector: "span:not(button span)", + }), + ); + + // Find the updated previewed JSON + const updatedPreviewOutput = screen.getByTestId("preview-json"); + + // Parse the updated JSON output and check the number of entries + const updatedJsonContent = JSON.parse(updatedPreviewOutput.textContent); + expect(updatedJsonContent.length).toBe(5); // Still 5 entries + + // Check that each entry now has 4 fields (3 original fields + 1 new field) + updatedJsonContent.forEach((entry) => { + expect(Object.keys(entry).length).toBe(4); // Fields count should be 4 after adding the new field + }); }); }); diff --git a/src/app/customizer/JsonGenerator/utils.js b/src/app/customizer/JsonGenerator/utils.js index 41deb9b7..f2a18e02 100644 --- a/src/app/customizer/JsonGenerator/utils.js +++ b/src/app/customizer/JsonGenerator/utils.js @@ -81,14 +81,30 @@ export default class Categories { } isDataCached(currentRowsRequested, previousResponseData, mappedSchema) { + // Ensure previousResponseData is valid and has data to compare + if ( + !previousResponseData || + previousResponseData.length === 0 || + !mappedSchema + ) { + return false; + } + + // Serialize the schema and the cached data structure for comparison + const previousSchema = Object.entries(previousResponseData[0]) + .map(([key, value]) => [key, typeof value]) + .sort() + .join(","); + const currentSchema = Object.entries(mappedSchema) + .map(([key, value]) => [key, typeof value()]) + .sort() + .join(","); + return ( - // Check if a request has already been made for data (check for cached data) - previousResponseData.length > 0 && - // Check if the requested data has changed from the previous data requested - Object.keys(mappedSchema).sort().join(",") == - Object.keys(previousResponseData[0]).sort().join(",") && + // Check if the schema (keys and types) matches + previousSchema === currentSchema && // Check if the number of rows requested is the same as the number of rows in the previous request - currentRowsRequested == previousResponseData.length + currentRowsRequested === previousResponseData.length ); } }