UNPKG

@adobe/spectrum-component-diff-generator

Version:

Generates diff reports for Spectrum component schema changes with breaking change analysis

320 lines (277 loc) 9.63 kB
/* Copyright 2024 Adobe. All rights reserved. This file is licensed to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ import test from "ava"; import Handlebars from "handlebars"; import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; import componentDiff from "../src/lib/component-diff.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Sample component schemas for testing const buttonSchema = { $schema: "http://json-schema.org/draft-07/schema#", $id: "https://example.com/button.json", title: "Button Component", type: "object", properties: { variant: { type: "string", enum: ["primary", "secondary"], }, }, required: ["variant"], }; const updatedButtonSchema = { ...buttonSchema, properties: { ...buttonSchema.properties, disabled: { type: "boolean" }, }, }; // Helper function to generate markdown report (copy from CLI) function generateMarkdownReport(diffResult, options = {}) { try { // Register necessary Handlebars helpers if (!Handlebars.helpers.hasKeys) { Handlebars.registerHelper("hasKeys", (obj) => { return obj && Object.keys(obj).length > 0; }); } // Load and compile the Handlebars template const templatePath = path.join(__dirname, "../templates/markdown.hbs"); const templateSource = fs.readFileSync(templatePath, "utf8"); const template = Handlebars.compile(templateSource); // Prepare template data const templateData = { summary: diffResult.summary, changes: diffResult.changes, options: options, }; // Render the template return template(templateData); } catch (error) { console.error(`Error generating markdown report: ${error.message}`); return `Error generating component diff report: ${error.message}`; } } // Template Error Handling Tests test("template error handling - missing template file", (t) => { const diffResult = componentDiff( { button: buttonSchema }, { button: updatedButtonSchema }, ); // Mock fs.readFileSync to throw file not found error const originalReadFileSync = fs.readFileSync; fs.readFileSync = () => { const error = new Error("ENOENT: no such file or directory"); error.code = "ENOENT"; throw error; }; try { const result = generateMarkdownReport(diffResult, {}); t.true(result.includes("Error generating component diff report")); t.true(result.includes("ENOENT")); } finally { fs.readFileSync = originalReadFileSync; } }); test("template error handling - malformed template syntax", (t) => { const diffResult = componentDiff( { button: buttonSchema }, { button: updatedButtonSchema }, ); // Mock fs.readFileSync to return malformed template const originalReadFileSync = fs.readFileSync; fs.readFileSync = () => { return "{{#if unclosed"; // Malformed Handlebars syntax }; try { const result = generateMarkdownReport(diffResult, {}); t.true(result.includes("Error generating component diff report")); } finally { fs.readFileSync = originalReadFileSync; } }); test("template error handling - undefined helper function", (t) => { const diffResult = componentDiff( { button: buttonSchema }, { button: updatedButtonSchema }, ); // Mock fs.readFileSync to return template with undefined helper const originalReadFileSync = fs.readFileSync; fs.readFileSync = () => { return "{{undefinedHelper changes.added}}"; }; try { const result = generateMarkdownReport(diffResult, {}); t.true(result.includes("Error generating component diff report")); } finally { fs.readFileSync = originalReadFileSync; } }); test("template error handling - null data passed to template", (t) => { // Pass null as diffResult to trigger template data issues const originalReadFileSync = fs.readFileSync; fs.readFileSync = () => { return "{{summary.hasBreakingChanges}}"; // Try to access property on null }; try { const result = generateMarkdownReport(null, {}); t.true(result.includes("Error generating component diff report")); } finally { fs.readFileSync = originalReadFileSync; } }); test("template error handling - missing template data properties", (t) => { // Create diffResult without expected properties const incompleteDiffResult = { summary: null, // Missing expected structure changes: undefined, }; try { const result = generateMarkdownReport(incompleteDiffResult, {}); // Should handle gracefully and not crash t.is(typeof result, "string"); } catch (error) { // If it throws, ensure it's handled gracefully t.true(error.message.includes("Cannot read")); } }); test("template error handling - file permission error", (t) => { const diffResult = componentDiff( { button: buttonSchema }, { button: updatedButtonSchema }, ); // Mock fs.readFileSync to throw permission error const originalReadFileSync = fs.readFileSync; fs.readFileSync = () => { const error = new Error("EACCES: permission denied"); error.code = "EACCES"; throw error; }; try { const result = generateMarkdownReport(diffResult, {}); t.true(result.includes("Error generating component diff report")); t.true(result.includes("EACCES")); } finally { fs.readFileSync = originalReadFileSync; } }); test("template error handling - empty template file", (t) => { const diffResult = componentDiff( { button: buttonSchema }, { button: updatedButtonSchema }, ); // Mock fs.readFileSync to return empty template const originalReadFileSync = fs.readFileSync; fs.readFileSync = () => ""; try { const result = generateMarkdownReport(diffResult, {}); // Empty template should render as empty string t.is(result, ""); } finally { fs.readFileSync = originalReadFileSync; } }); test("template error handling - special characters in branch names", (t) => { const diffResult = componentDiff( { button: buttonSchema }, { button: updatedButtonSchema }, ); const options = { oldSchemaBranch: "feature/test-special-chars", newSchemaBranch: "main", oldSchemaVersion: "v1.0.0-beta.1", newSchemaVersion: "v2.0.0-rc.1", }; try { const result = generateMarkdownReport(diffResult, options); // Should handle special characters gracefully (or fail gracefully) if (result.includes("Error generating component diff report")) { // If template fails, ensure error handling works t.true(result.includes("Error generating component diff report")); } else { // If template succeeds, verify content is rendered t.true(result.includes("feature/test-special-chars")); t.true(result.includes("v1.0.0-beta.1")); } } catch (error) { t.fail(`Should handle special characters gracefully: ${error.message}`); } }); test("template error handling - unicode characters in component names", (t) => { const unicodeButtonSchema = { ...buttonSchema, title: "按钮组件 (Button Component 🔘)", }; const diffResult = componentDiff( { "button-测试": unicodeButtonSchema }, { "button-测试": updatedButtonSchema }, ); try { const result = generateMarkdownReport(diffResult, {}); // Should handle unicode characters gracefully t.true(result.includes("button-测试")); t.true(typeof result === "string"); t.true(result.length > 0); } catch (error) { t.fail(`Should handle unicode characters gracefully: ${error.message}`); } }); test("template error handling - very large component schemas", (t) => { // Create a schema with many properties to test performance/memory const largeProperties = {}; for (let i = 0; i < 1000; i++) { largeProperties[`property${i}`] = { type: "string" }; } const largeSchema = { ...buttonSchema, properties: largeProperties, }; const diffResult = componentDiff( { largeComponent: largeSchema }, { largeComponent: { ...largeSchema, properties: { ...largeProperties, newProp: { type: "boolean" } }, }, }, ); try { const result = generateMarkdownReport(diffResult, {}); // Should handle large schemas without error t.true(typeof result === "string"); t.true(result.includes("largeComponent")); } catch (error) { t.fail(`Should handle large schemas gracefully: ${error.message}`); } }); test("template error handling - circular reference in options", (t) => { const diffResult = componentDiff( { button: buttonSchema }, { button: updatedButtonSchema }, ); // Create circular reference in options const circularOptions = { oldSchemaBranch: "main", }; circularOptions.circular = circularOptions; try { const result = generateMarkdownReport(diffResult, circularOptions); // Should handle without infinite recursion t.true(typeof result === "string"); t.true(result.includes("main")); } catch (error) { // If it fails, ensure it's not an infinite recursion t.false(error.message.includes("Maximum call stack")); } });