diff --git a/docs/yaml-examples.md b/docs/yaml-examples.md
new file mode 100644
index 0000000000..23ffc192aa
--- /dev/null
+++ b/docs/yaml-examples.md
@@ -0,0 +1,339 @@
+# Example: Using YAML Support in SurveyJS Creator
+
+This example demonstrates how to use the new YAML support features.
+
+## HTML Example
+
+```html
+
+
+
+ Survey Creator with YAML Support
+
+
+
+
+
+
+
+
+
+```
+
+## React Example
+
+```jsx
+import React, { useRef, useEffect } from 'react';
+import { SurveyCreatorModel } from 'survey-creator-core';
+
+const YAMLSurveyCreator = () => {
+ const creatorRef = useRef(null);
+ const creator = useRef(null);
+
+ useEffect(() => {
+ // Initialize creator
+ creator.current = new SurveyCreatorModel({
+ showJSONEditorTab: true
+ });
+
+ // Sample YAML
+ const yamlSurvey = `
+title: React YAML Survey
+pages:
+ - name: page1
+ elements:
+ - type: text
+ name: userName
+ title: Your Name
+ - type: rating
+ name: experience
+ title: Rate your experience
+ rateMax: 5
+`;
+
+ // Load YAML
+ creator.current.yamlText = yamlSurvey;
+
+ // Render
+ creator.current.render(creatorRef.current);
+
+ return () => {
+ creator.current?.dispose();
+ };
+ }, []);
+
+ const exportToYAML = () => {
+ creator.current?.exportToYAMLFile('react-survey.yml');
+ };
+
+ const importFromYAML = () => {
+ creator.current?.importFromYAMLFileDOM();
+ };
+
+ const logCurrentYAML = () => {
+ console.log('Current YAML:');
+ console.log(creator.current?.yamlText);
+ };
+
+ return (
+
+
+ Export YAML
+ Import YAML
+ Log YAML
+
+
+
+ );
+};
+
+export default YAMLSurveyCreator;
+```
+
+## Node.js Example
+
+```javascript
+const { SurveyYAML, SurveyCreatorModel } = require('survey-creator-core');
+const fs = require('fs');
+
+// Read YAML file
+const yamlContent = fs.readFileSync('survey.yml', 'utf8');
+
+// Parse YAML to object
+const surveyDefinition = SurveyYAML.parse(yamlContent);
+
+// Create creator and set survey
+const creator = new SurveyCreatorModel();
+creator.JSON = surveyDefinition;
+
+// Make some modifications
+creator.survey.title = "Modified Survey Title";
+
+// Export back to YAML
+const updatedYAML = creator.yamlText;
+fs.writeFileSync('updated-survey.yml', updatedYAML);
+
+console.log('Survey processed and saved');
+```
+
+## Converting Existing JSON Surveys to YAML
+
+```javascript
+const { SurveyYAML } = require('survey-creator-core');
+const fs = require('fs');
+
+// Read existing JSON survey
+const jsonContent = fs.readFileSync('existing-survey.json', 'utf8');
+
+// Convert to YAML
+const yamlContent = SurveyYAML.jsonToYaml(jsonContent);
+
+// Save as YAML file
+fs.writeFileSync('converted-survey.yml', yamlContent);
+
+console.log('Conversion complete!');
+```
+
+## Error Handling Example
+
+```javascript
+function loadSurveyFromYAML(yamlString) {
+ try {
+ // Validate YAML first
+ if (!SurveyYAML.isValidYAML(yamlString)) {
+ throw new Error('Invalid YAML format');
+ }
+
+ // Parse YAML
+ const surveyObj = SurveyYAML.parse(yamlString);
+
+ // Validate required fields
+ if (!surveyObj.pages || !Array.isArray(surveyObj.pages)) {
+ throw new Error('Survey must have pages array');
+ }
+
+ // Set in creator
+ creator.JSON = surveyObj;
+
+ return { success: true };
+ } catch (error) {
+ console.error('Failed to load survey:', error.message);
+ return {
+ success: false,
+ error: error.message
+ };
+ }
+}
+```
+
+## Sample YAML Survey Files
+
+### Basic Survey
+```yaml
+# survey-basic.yml
+title: Basic Customer Survey
+description: We value your feedback
+pages:
+ - name: feedback
+ elements:
+ - type: text
+ name: name
+ title: Your Name
+ isRequired: true
+ - type: rating
+ name: satisfaction
+ title: Satisfaction Rating
+ rateMax: 5
+ - type: comment
+ name: comments
+ title: Additional Comments
+```
+
+### Advanced Survey with Logic
+```yaml
+# survey-advanced.yml
+title: Product Feedback Survey
+pages:
+ - name: product-usage
+ elements:
+ - type: radiogroup
+ name: hasUsedProduct
+ title: Have you used our product?
+ choices:
+ - value: yes
+ text: "Yes"
+ - value: no
+ text: "No"
+
+ - type: dropdown
+ name: productVersion
+ title: Which version did you use?
+ visibleIf: "{hasUsedProduct} = 'yes'"
+ choices:
+ - v1.0
+ - v2.0
+ - v3.0
+
+ - type: matrix
+ name: features
+ title: Rate these features
+ visibleIf: "{hasUsedProduct} = 'yes'"
+ columns:
+ - value: 1
+ text: Poor
+ - value: 2
+ text: Fair
+ - value: 3
+ text: Good
+ - value: 4
+ text: Very Good
+ - value: 5
+ text: Excellent
+ rows:
+ - value: usability
+ text: Ease of Use
+ - value: performance
+ text: Performance
+ - value: design
+ text: Design
+
+triggers:
+ - type: skip
+ expression: "{hasUsedProduct} = 'no'"
+ gotoName: feedback
+
+ - type: complete
+ expression: "{hasUsedProduct} = 'yes' and {features.usability} >= 4"
+
+calculatedValues:
+ - name: avgRating
+ expression: "({features.usability} + {features.performance} + {features.design}) / 3"
+```
\ No newline at end of file
diff --git a/docs/yaml-support.md b/docs/yaml-support.md
new file mode 100644
index 0000000000..10aa94bab2
--- /dev/null
+++ b/docs/yaml-support.md
@@ -0,0 +1,240 @@
+# YAML Support for SurveyJS Creator
+
+This document describes the YAML support functionality added to SurveyJS Creator.
+
+## Overview
+
+YAML support allows users to work with survey definitions in YAML format as an alternative to JSON. This includes:
+
+- Converting between JSON and YAML formats
+- Importing survey definitions from YAML files
+- Exporting survey definitions to YAML files
+- YAML editor tab in the Creator UI
+
+## Key Components
+
+### 1. SurveyYAML Utility Class (`src/yaml-utils.ts`)
+
+The core utility for YAML operations:
+
+```typescript
+import { SurveyYAML } from 'survey-creator-core';
+
+// Parse YAML string to object
+const surveyObj = SurveyYAML.parse(yamlString);
+
+// Convert object to YAML string
+const yamlString = SurveyYAML.stringify(surveyObj);
+
+// Validate YAML
+const isValid = SurveyYAML.isValidYAML(yamlString);
+
+// Convert between formats
+const yamlFromJson = SurveyYAML.jsonToYaml(jsonString);
+const jsonFromYaml = SurveyYAML.yamlToJson(yamlString);
+```
+
+### 2. SurveyCreatorBase Extensions
+
+New properties and methods added to the main creator class:
+
+```typescript
+import { SurveyCreatorModel } from 'survey-creator-core';
+
+const creator = new SurveyCreatorModel();
+
+// Get/Set survey as YAML
+creator.yamlText = yamlString;
+const yamlString = creator.yamlText;
+
+// Export/Import methods
+creator.exportToYAMLFile('my-survey.yml');
+creator.importFromYAMLFileDOM(); // Opens file dialog
+
+// Programmatic import
+creator.importFromYAMLFile(file).then(() => {
+ console.log('Survey imported from YAML');
+});
+
+// Utility methods
+const yamlString = creator.toYAML();
+creator.fromYAML(yamlString);
+```
+
+### 3. YAML Editor Plugin (`src/components/tabs/yaml-editor-plugin.ts`)
+
+A new editor tab that extends the JSON editor functionality:
+
+```typescript
+import { TabYamlEditorPlugin } from 'survey-creator-core';
+
+// The plugin provides:
+// - YAML syntax highlighting (when integrated with code editors)
+// - YAML validation with error reporting
+// - Export/Import actions in the toolbar
+// - Real-time conversion between YAML and survey model
+```
+
+### 4. File Operations (`src/components/yaml-export-import.ts`)
+
+Helper functions for file import/export:
+
+```typescript
+import { YAMLFileHelper, createExportYAMLAction, createImportYAMLAction } from 'survey-creator-core';
+
+// Create actions for toolbars
+const exportAction = createExportYAMLAction(() => {
+ // Export logic
+});
+
+const importAction = createImportYAMLAction(() => {
+ // Import logic
+}, true); // needSeparator
+
+// File operations
+YAMLFileHelper.exportToYAMLFile(surveyObj, 'survey.yml');
+YAMLFileHelper.importFromYAMLFile(file).then(obj => {
+ // Handle imported object
+});
+```
+
+## Usage Examples
+
+### Basic Survey Definition in YAML
+
+```yaml
+title: Customer Satisfaction Survey
+description: Please help us improve our service
+pages:
+ - name: page1
+ title: Basic Information
+ elements:
+ - type: text
+ name: firstName
+ title: First Name
+ isRequired: true
+ - type: text
+ name: lastName
+ title: Last Name
+ isRequired: true
+ - type: dropdown
+ name: country
+ title: Country
+ choices:
+ - value: us
+ text: United States
+ - value: ca
+ text: Canada
+ - value: uk
+ text: United Kingdom
+
+ - name: page2
+ title: Feedback
+ elements:
+ - type: rating
+ name: satisfaction
+ title: Overall Satisfaction
+ rateMin: 1
+ rateMax: 10
+ rateStep: 1
+ - type: comment
+ name: suggestions
+ title: Additional Comments
+ placeholder: Please share your suggestions...
+```
+
+### Advanced Features
+
+```yaml
+# Survey with logic and calculated values
+title: Advanced Survey Example
+pages:
+ - name: demographics
+ elements:
+ - type: radiogroup
+ name: hasChildren
+ title: Do you have children?
+ choices:
+ - value: yes
+ text: "Yes"
+ - value: no
+ text: "No"
+
+ - type: text
+ name: numberOfChildren
+ title: How many children do you have?
+ inputType: number
+ visibleIf: "{hasChildren} = 'yes'"
+
+# Triggers
+triggers:
+ - type: complete
+ expression: "{satisfaction} >= 8"
+
+# Calculated values
+calculatedValues:
+ - name: totalScore
+ expression: "{satisfaction} + {likelihood}"
+
+# Custom CSS
+css:
+ question:
+ title: "font-weight: bold;"
+```
+
+### Error Handling
+
+```typescript
+try {
+ const surveyObj = SurveyYAML.parse(yamlString);
+ creator.JSON = surveyObj;
+} catch (error) {
+ console.error('YAML parsing failed:', error.message);
+ // Handle error - show user-friendly message
+}
+```
+
+## Integration with Existing Code
+
+The YAML support is designed to be a seamless extension of existing JSON functionality:
+
+1. **No Breaking Changes**: All existing JSON APIs continue to work
+2. **Interchangeable Formats**: Easily convert between JSON and YAML
+3. **Consistent API**: YAML methods follow the same patterns as JSON methods
+4. **Error Handling**: Comprehensive error messages for debugging
+
+## File Format Support
+
+Supported file extensions:
+- `.yaml`
+- `.yml`
+
+MIME type: `text/yaml`
+
+## Localization
+
+New localization keys added to English translation:
+- `yamlExportButton`: "Export to YAML"
+- `yamlImportButton`: "Import from YAML"
+
+## Testing
+
+Comprehensive test coverage includes:
+- YAML parsing and stringification
+- Round-trip conversion (JSON ↔ YAML ↔ JSON)
+- Error handling for invalid YAML
+- File import/export operations
+- Special characters and Unicode support
+- Empty and null value handling
+
+## Browser Compatibility
+
+The YAML functionality works in all modern browsers that support:
+- ES6 features (Promise, const/let, arrow functions)
+- File API for file import/export
+- Blob API for file downloads
+
+## Dependencies
+
+- `js-yaml`: ^4.1.0 - Core YAML parsing and stringification
+- `@types/js-yaml`: ^4.0.5 - TypeScript type definitions
\ No newline at end of file
diff --git a/packages/survey-creator-core/package.json b/packages/survey-creator-core/package.json
index 723b507956..0b17b2d3a7 100644
--- a/packages/survey-creator-core/package.json
+++ b/packages/survey-creator-core/package.json
@@ -29,6 +29,7 @@
"lint:fix": "eslint . --max-warnings=0 --fix"
},
"dependencies": {
+ "js-yaml": "^4.1.0",
"survey-core": "../../../survey-library/packages/survey-core/build"
},
"devDependencies": {
@@ -38,6 +39,7 @@
"@rollup/plugin-typescript": "^12.1.2",
"@types/ace": "0.0.32",
"@types/jest": "^26.0.24",
+ "@types/js-yaml": "^4.0.9",
"@types/node": "7.0.4",
"@types/papaparse": "^5.0.4",
"@types/qunit": "2.0.31",
diff --git a/packages/survey-creator-core/src/components/tabs/yaml-editor-plugin.ts b/packages/survey-creator-core/src/components/tabs/yaml-editor-plugin.ts
new file mode 100644
index 0000000000..96bbbfe6dd
--- /dev/null
+++ b/packages/survey-creator-core/src/components/tabs/yaml-editor-plugin.ts
@@ -0,0 +1,162 @@
+import { Action } from "survey-core";
+import { SurveyCreatorModel } from "../../creator-base";
+import { ICreatorPlugin } from "../../creator-settings";
+import { JsonEditorBaseModel, TabJsonEditorBasePlugin } from "./json-editor-plugin";
+import { SurveyYAML } from "../../yaml-utils";
+import { saveToFileHandler } from "../../utils/html-element-utils";
+
+export class YamlEditorModel extends JsonEditorBaseModel {
+ protected getText(): string {
+ try {
+ return this.creator.yamlText;
+ } catch (error) {
+ return "# Error converting to YAML: " + (error as Error).message;
+ }
+ }
+
+ protected setText(val: string): void {
+ try {
+ this.creator.yamlText = val;
+ this.isJSONChanged = true;
+ } catch (error) {
+ // Don't set the text if there's an error, just mark as changed
+ this.isJSONChanged = true;
+ }
+ }
+
+ public processErrors(text: string): void {
+ const errors: any[] = [];
+ try {
+ SurveyYAML.parse(text);
+ this.setErrors([]);
+ } catch (error) {
+ errors.push({
+ position: { start: 0, end: text.length },
+ text: (error as Error).message
+ });
+ this.setErrors(errors);
+ }
+ }
+}
+
+export class TabYamlEditorPlugin extends TabJsonEditorBasePlugin implements ICreatorPlugin {
+ public static id = "yaml-editor";
+ private yamlModel: YamlEditorModel;
+
+ constructor(creator: SurveyCreatorModel) {
+ super(creator);
+ this.yamlModel = new YamlEditorModel(creator);
+ }
+
+ public get model(): YamlEditorModel {
+ return this.yamlModel;
+ }
+
+ public activate(): void {
+ this.model.onPluginActivate();
+ }
+
+ public deactivate(): boolean {
+ if (this.model.isJSONChanged) {
+ this.model.processErrors(this.model.text);
+ if (!this.model.hasErrors) {
+ this.creator.yamlText = this.model.text;
+ }
+ this.model.isJSONChanged = false;
+ }
+ return true;
+ }
+
+ public createActions(): Action[] {
+ const actions: Action[] = [];
+
+ // Export to YAML file action
+ const exportAction = new Action({
+ id: "svd-yaml-export",
+ iconName: "icon-download",
+ iconSize: "auto",
+ locTitleName: "ed.yamlExportButton",
+ locTooltipName: "ed.yamlExportButton",
+ mode: "small",
+ component: "sv-action-bar-item",
+ action: () => {
+ this.exportToYAMLFile("survey.yml");
+ }
+ });
+ actions.push(exportAction);
+
+ // Import from YAML file action
+ const importAction = new Action({
+ id: "svd-yaml-import",
+ iconName: "icon-load",
+ iconSize: "auto",
+ locTitleName: "ed.yamlImportButton",
+ locTooltipName: "ed.yamlImportButton",
+ mode: "small",
+ component: "sv-action-bar-item",
+ action: () => {
+ this.importFromYAMLFileDOM();
+ }
+ });
+ actions.push(importAction);
+
+ // Copy YAML to clipboard action
+ const copyAction = new Action({
+ id: "svd-yaml-copy",
+ iconName: "icon-copy",
+ iconSize: "auto",
+ locTitleName: "ed.copyToClipboard",
+ locTooltipName: "ed.copyToClipboard",
+ mode: "small",
+ component: "sv-action-bar-item",
+ action: () => {
+ if (typeof navigator !== "undefined" && navigator.clipboard) {
+ navigator.clipboard.writeText(this.model.text);
+ }
+ }
+ });
+ actions.push(copyAction);
+
+ return actions;
+ }
+
+ public exportToYAMLFile(fileName: string): void {
+ if (this.model) {
+ const yamlBlob = new Blob([this.model.text], { type: "text/yaml" });
+ saveToFileHandler(fileName, yamlBlob);
+ }
+ }
+
+ public importFromYAMLFile(file: File, callback?: (yaml: string) => void): void {
+ const fileReader = new FileReader();
+ fileReader.onload = (e) => {
+ const yamlText = fileReader.result as string;
+ if (this.model) {
+ this.model.text = yamlText;
+ this.model.isJSONChanged = true;
+ }
+ if (callback) callback(yamlText);
+ };
+ fileReader.readAsText(file);
+ }
+
+ public importFromYAMLFileDOM(): void {
+ if (typeof document === "undefined") return;
+
+ const inputElement = document.createElement("input");
+ inputElement.type = "file";
+ inputElement.accept = ".yaml,.yml";
+ inputElement.style.display = "none";
+
+ inputElement.onchange = () => {
+ if (inputElement.files && inputElement.files.length > 0) {
+ this.importFromYAMLFile(inputElement.files[0]);
+ inputElement.value = ""; // Reset for subsequent imports
+ }
+ };
+
+ document.body.appendChild(inputElement);
+ inputElement.click();
+ document.body.removeChild(inputElement);
+ }
+}
\ No newline at end of file
diff --git a/packages/survey-creator-core/src/components/yaml-export-import.ts b/packages/survey-creator-core/src/components/yaml-export-import.ts
new file mode 100644
index 0000000000..551565b318
--- /dev/null
+++ b/packages/survey-creator-core/src/components/yaml-export-import.ts
@@ -0,0 +1,113 @@
+import { Action } from "survey-core";
+import { SurveyYAML } from "../yaml-utils";
+
+/**
+ * Creates an action for exporting survey to YAML file
+ */
+export function createExportYAMLAction(action: () => void, isInEditor: boolean = false): Action {
+ return new Action({
+ id: "svc-yaml-export",
+ iconName: "icon-download",
+ iconSize: "auto",
+ locTitleName: "ed.yamlExportButton",
+ locTooltipName: "ed.yamlExportButton",
+ mode: isInEditor ? "large" : "small",
+ component: "sv-action-bar-item",
+ action: action
+ });
+}
+
+/**
+ * Creates an action for importing survey from YAML file
+ */
+export function createImportYAMLAction(action: () => void, needSeparator: boolean, isInEditor: boolean = false): Action {
+ return new Action({
+ id: "svc-yaml-import",
+ iconName: "icon-load",
+ iconSize: "auto",
+ locTitleName: "ed.yamlImportButton",
+ locTooltipName: "ed.yamlImportButton",
+ mode: isInEditor ? "large" : "small",
+ component: "sv-action-bar-item",
+ needSeparator: needSeparator,
+ action: action
+ });
+}
+
+/**
+ * Helper class for YAML file operations
+ */
+export class YAMLFileHelper {
+ /**
+ * Export object to YAML file
+ */
+ public static exportToYAMLFile(obj: any, fileName: string): void {
+ if (typeof window === "undefined") return;
+
+ try {
+ const yamlData = SurveyYAML.stringify(obj);
+ const blob = new Blob([yamlData], { type: "text/yaml" });
+
+ if ((window.navigator as any)["msSaveOrOpenBlob"]) {
+ (window.navigator as any)["msSaveBlob"](blob, fileName);
+ } else {
+ const elem = window.document.createElement("a");
+ elem.href = window.URL.createObjectURL(blob);
+ elem.download = fileName;
+ document.body.appendChild(elem);
+ elem.click();
+ document.body.removeChild(elem);
+ }
+ } catch (error) {
+ throw new Error(`Failed to export YAML file: ${(error as Error).message}`);
+ }
+ }
+
+ /**
+ * Import YAML file and parse to object
+ */
+ public static importFromYAMLFile(file: File): Promise {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+
+ reader.onload = (event) => {
+ try {
+ const yamlString = event.target?.result as string;
+ const obj = SurveyYAML.parse(yamlString);
+ resolve(obj);
+ } catch (error) {
+ reject(new Error(`Failed to parse YAML file: ${(error as Error).message}`));
+ }
+ };
+
+ reader.onerror = () => {
+ reject(new Error("Failed to read file"));
+ };
+
+ reader.readAsText(file);
+ });
+ }
+
+ /**
+ * Create file input element for YAML import
+ */
+ public static createFileInput(onFileSelected: (file: File) => void): HTMLInputElement {
+ if (typeof document === "undefined") {
+ throw new Error("Document is not available");
+ }
+
+ const inputElement = document.createElement("input");
+ inputElement.type = "file";
+ inputElement.accept = ".yaml,.yml";
+ inputElement.style.display = "none";
+
+ inputElement.onchange = () => {
+ if (inputElement.files && inputElement.files.length > 0) {
+ onFileSelected(inputElement.files[0]);
+ inputElement.value = ""; // Reset for subsequent imports
+ }
+ };
+
+ return inputElement;
+ }
+}
\ No newline at end of file
diff --git a/packages/survey-creator-core/src/creator-base.ts b/packages/survey-creator-core/src/creator-base.ts
index 4fa2f9cf4f..016df2b994 100644
--- a/packages/survey-creator-core/src/creator-base.ts
+++ b/packages/survey-creator-core/src/creator-base.ts
@@ -14,6 +14,7 @@ import {
import { ICreatorPlugin, ISurveyCreatorOptions, settings, ICollectionItemAllowOperations, ITabOptions } from "./creator-settings";
import { editorLocalization, setupLocale } from "./editorLocalization";
import { SurveyJSON5 } from "./json5";
+import { SurveyYAML } from "./yaml-utils";
import { DragDropChoices } from "survey-core";
import { IsTouch } from "survey-core";
import { QuestionConverter } from "./questionconverter";
@@ -2906,6 +2907,118 @@ export class SurveyCreatorModel extends Base
this.initSurveyWithJSON(val, true);
}
}
+
+ /**
+ * A survey YAML schema as a string.
+ *
+ * This property allows you to get or set the YAML schema of a survey being configured.
+ */
+ public get yamlText(): string {
+ try {
+ const json = this.JSON;
+ return SurveyYAML.stringify(json);
+ } catch (error) {
+ throw new Error(`Failed to convert survey to YAML: ${error.message}`);
+ }
+ }
+ public set yamlText(value: string) {
+ try {
+ const obj = SurveyYAML.parse(value);
+ this.JSON = obj;
+ } catch (error) {
+ throw new Error(`Failed to parse YAML: ${error.message}`);
+ }
+ }
+
+ /**
+ * Export survey as YAML string
+ */
+ public toYAML(): string {
+ return this.yamlText;
+ }
+
+ /**
+ * Import survey from YAML string
+ */
+ public fromYAML(yamlString: string): void {
+ this.yamlText = yamlString;
+ }
+
+ /**
+ * Export survey definition to YAML file
+ */
+ public exportToYAMLFile(fileName: string = "survey.yml"): void {
+ try {
+ const yamlData = this.yamlText;
+ if (typeof window === "undefined") return;
+
+ const blob = new Blob([yamlData], { type: "text/yaml" });
+
+ if ((window.navigator as any)["msSaveOrOpenBlob"]) {
+ (window.navigator as any)["msSaveBlob"](blob, fileName);
+ } else {
+ const elem = window.document.createElement("a");
+ elem.href = window.URL.createObjectURL(blob);
+ elem.download = fileName;
+ document.body.appendChild(elem);
+ elem.click();
+ document.body.removeChild(elem);
+ }
+ } catch (error) {
+ throw new Error(`Failed to export YAML file: ${(error as Error).message}`);
+ }
+ }
+
+ /**
+ * Import survey definition from YAML file
+ */
+ public importFromYAMLFile(file: File): Promise {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+
+ reader.onload = (event) => {
+ try {
+ const yamlString = event.target?.result as string;
+ this.yamlText = yamlString;
+ resolve();
+ } catch (error) {
+ reject(new Error(`Failed to parse YAML file: ${(error as Error).message}`));
+ }
+ };
+
+ reader.onerror = () => {
+ reject(new Error("Failed to read file"));
+ };
+
+ reader.readAsText(file);
+ });
+ }
+
+ /**
+ * Create DOM file input for YAML import
+ */
+ public importFromYAMLFileDOM(): void {
+ if (typeof document === "undefined") return;
+
+ const inputElement = document.createElement("input");
+ inputElement.type = "file";
+ inputElement.accept = ".yaml,.yml";
+ inputElement.style.display = "none";
+
+ inputElement.onchange = () => {
+ if (inputElement.files && inputElement.files.length > 0) {
+ this.importFromYAMLFile(inputElement.files[0]).catch(error => {
+ console.error("Failed to import YAML file:", error);
+ // You could emit an event here for error handling
+ });
+ inputElement.value = ""; // Reset for subsequent imports
+ }
+ };
+
+ document.body.appendChild(inputElement);
+ inputElement.click();
+ document.body.removeChild(inputElement);
+ }
public loadSurvey(surveyId: string): void {
// eslint-disable-next-line no-console
console.warn("Self-hosted Form Library no longer supports integration with SurveyJS Demo Service. Learn more: https://surveyjs.io/stay-updated/release-notes/v2.0.0#form-library-removes-apis-for-integration-with-surveyjs-demo-service");
diff --git a/packages/survey-creator-core/src/entries/index.ts b/packages/survey-creator-core/src/entries/index.ts
index 7e9f405fc1..54b9be5c78 100644
--- a/packages/survey-creator-core/src/entries/index.ts
+++ b/packages/survey-creator-core/src/entries/index.ts
@@ -18,6 +18,7 @@ export * from "../creator-responsivity-manager";
export * from "../components/tabs/json-editor-ace";
export * from "../components/tabs/json-editor-plugin";
export * from "../components/tabs/json-editor-textarea";
+export * from "../components/tabs/yaml-editor-plugin";
export * from "../components/tabs/test";
export * from "../components/tabs/test-plugin";
export * from "../components/tabs/theme-custom-questions/color-alpha";
@@ -99,6 +100,8 @@ export { MenuButton } from "../utils/actions";
export * from "../question-editor/definition";
export * from "../question-editor/properties";
export * from "../survey-helper";
+export * from "../yaml-utils";
+export * from "../components/yaml-export-import";
export * from "../utils/resizer";
export * from "../plugins/undo-redo";
export * from "../plugins/undo-redo/undo-redo-manager";
diff --git a/packages/survey-creator-core/src/localization/english.ts b/packages/survey-creator-core/src/localization/english.ts
index 5c7a629c56..56c8c64cf8 100644
--- a/packages/survey-creator-core/src/localization/english.ts
+++ b/packages/survey-creator-core/src/localization/english.ts
@@ -163,6 +163,8 @@ export var enStrings = {
translationNoStrings: "No strings to translate. Please change the filter.",
translationExportToSCVButton: "Export to CSV",
translationImportFromSCVButton: "Import from CSV",
+ yamlExportButton: "Export to YAML",
+ yamlImportButton: "Import from YAML",
translateUsigAI: "Auto-translate All",
translateUsigAIFrom: "Translate from: ",
translationDialogTitle: "Untranslated strings",
diff --git a/packages/survey-creator-core/src/yaml-utils.ts b/packages/survey-creator-core/src/yaml-utils.ts
new file mode 100644
index 0000000000..ade299c235
--- /dev/null
+++ b/packages/survey-creator-core/src/yaml-utils.ts
@@ -0,0 +1,83 @@
+import * as yaml from "js-yaml";
+
+/**
+ * Utility class for YAML operations in Survey Creator
+ */
+export class SurveyYAML {
+ /**
+ * Parse YAML string to JavaScript object
+ * @param yamlString - The YAML string to parse
+ * @returns Parsed JavaScript object
+ * @throws Error if YAML parsing fails
+ */
+ public static parse(yamlString: string): any {
+ try {
+ return yaml.load(yamlString);
+ } catch (error) {
+ throw new Error(`YAML parsing error: ${error.message}`);
+ }
+ }
+
+ /**
+ * Convert JavaScript object to YAML string
+ * @param obj - The object to convert to YAML
+ * @param options - Optional YAML stringification options
+ * @returns YAML string representation
+ */
+ public static stringify(obj: any, options?: yaml.DumpOptions): string {
+ try {
+ const defaultOptions: yaml.DumpOptions = {
+ indent: 2,
+ lineWidth: 120,
+ noRefs: true,
+ sortKeys: false,
+ ...options
+ };
+ return yaml.dump(obj, defaultOptions);
+ } catch (error) {
+ throw new Error(`YAML stringification error: ${error.message}`);
+ }
+ }
+
+ /**
+ * Check if a string is valid YAML
+ * @param yamlString - The string to validate
+ * @returns true if valid YAML, false otherwise
+ */
+ public static isValidYAML(yamlString: string): boolean {
+ try {
+ yaml.load(yamlString);
+ return true;
+ } catch {
+ return false;
+ }
+ }
+
+ /**
+ * Convert JSON string to YAML string
+ * @param jsonString - JSON string to convert
+ * @returns YAML string
+ */
+ public static jsonToYaml(jsonString: string): string {
+ try {
+ const obj = JSON.parse(jsonString);
+ return this.stringify(obj);
+ } catch (error) {
+ throw new Error(`JSON to YAML conversion error: ${error.message}`);
+ }
+ }
+
+ /**
+ * Convert YAML string to JSON string
+ * @param yamlString - YAML string to convert
+ * @returns JSON string
+ */
+ public static yamlToJson(yamlString: string): string {
+ try {
+ const obj = this.parse(yamlString);
+ return JSON.stringify(obj, null, 2);
+ } catch (error) {
+ throw new Error(`YAML to JSON conversion error: ${error.message}`);
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/survey-creator-core/tests/yaml-file-operations.tests.ts b/packages/survey-creator-core/tests/yaml-file-operations.tests.ts
new file mode 100644
index 0000000000..217f08cfaa
--- /dev/null
+++ b/packages/survey-creator-core/tests/yaml-file-operations.tests.ts
@@ -0,0 +1,272 @@
+import { YAMLFileHelper, createExportYAMLAction, createImportYAMLAction } from "../src/components/yaml-export-import";
+
+// Mock DOM environment for testing
+const mockWindow = {
+ document: {
+ createElement: jest.fn(),
+ body: {
+ appendChild: jest.fn(),
+ removeChild: jest.fn()
+ }
+ },
+ URL: {
+ createObjectURL: jest.fn(),
+ revokeObjectURL: jest.fn()
+ },
+ navigator: {}
+};
+
+const mockBlob = jest.fn();
+const mockFileReader = jest.fn();
+
+// Mock globals
+Object.defineProperty(global, 'window', { value: mockWindow });
+Object.defineProperty(global, 'document', { value: mockWindow.document });
+Object.defineProperty(global, 'Blob', { value: mockBlob });
+Object.defineProperty(global, 'FileReader', { value: mockFileReader });
+
+describe("YAML Export/Import Functionality", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+
+ // Setup Blob mock
+ mockBlob.mockImplementation((content, options) => ({
+ content,
+ type: options.type
+ }));
+
+ // Setup createElement mock
+ mockWindow.document.createElement.mockImplementation((tagName) => {
+ if (tagName === 'input') {
+ return {
+ type: '',
+ accept: '',
+ style: { display: '' },
+ value: '',
+ files: null,
+ onchange: null,
+ click: jest.fn()
+ };
+ }
+ if (tagName === 'a') {
+ return {
+ href: '',
+ download: '',
+ click: jest.fn()
+ };
+ }
+ return {};
+ });
+
+ // Setup URL mock
+ mockWindow.URL.createObjectURL.mockReturnValue('blob:mock-url');
+ });
+
+ describe("YAMLFileHelper", () => {
+ test("should create export YAML action", () => {
+ const mockAction = jest.fn();
+ const action = createExportYAMLAction(mockAction, false);
+
+ expect(action).toBeDefined();
+ expect(action.id).toBe("svc-yaml-export");
+ expect(action.iconName).toBe("icon-download");
+ expect(action.locTitleName).toBe("ed.yamlExportButton");
+ expect(action.mode).toBe("small");
+ });
+
+ test("should create export YAML action for editor", () => {
+ const mockAction = jest.fn();
+ const action = createExportYAMLAction(mockAction, true);
+
+ expect(action.mode).toBe("large");
+ });
+
+ test("should create import YAML action", () => {
+ const mockAction = jest.fn();
+ const action = createImportYAMLAction(mockAction, true, false);
+
+ expect(action).toBeDefined();
+ expect(action.id).toBe("svc-yaml-import");
+ expect(action.iconName).toBe("icon-load");
+ expect(action.locTitleName).toBe("ed.yamlImportButton");
+ expect(action.needSeparator).toBe(true);
+ expect(action.mode).toBe("small");
+ });
+
+ test("should export to YAML file", () => {
+ const testObj = {
+ title: "Test Survey",
+ pages: [{
+ name: "page1",
+ elements: [{
+ type: "text",
+ name: "question1"
+ }]
+ }]
+ };
+
+ YAMLFileHelper.exportToYAMLFile(testObj, "test-survey.yml");
+
+ expect(mockBlob).toHaveBeenCalledWith(
+ expect.stringContaining("title: Test Survey"),
+ { type: "text/yaml" }
+ );
+ expect(mockWindow.document.createElement).toHaveBeenCalledWith("a");
+ expect(mockWindow.URL.createObjectURL).toHaveBeenCalled();
+ expect(mockWindow.document.body.appendChild).toHaveBeenCalled();
+ expect(mockWindow.document.body.removeChild).toHaveBeenCalled();
+ });
+
+ test("should handle export error gracefully", () => {
+ // Mock an object that will cause YAML stringify to fail
+ const circularObj = {};
+ circularObj.self = circularObj;
+
+ expect(() => {
+ YAMLFileHelper.exportToYAMLFile(circularObj, "test.yml");
+ }).toThrow("Failed to export YAML file");
+ });
+
+ test("should create file input for import", () => {
+ const mockCallback = jest.fn();
+ const mockInput = {
+ type: '',
+ accept: '',
+ style: { display: '' },
+ onchange: null
+ };
+
+ mockWindow.document.createElement.mockReturnValue(mockInput);
+
+ const input = YAMLFileHelper.createFileInput(mockCallback);
+
+ expect(input.type).toBe("file");
+ expect(input.accept).toBe(".yaml,.yml");
+ expect(input.style.display).toBe("none");
+ expect(input.onchange).toBeDefined();
+ });
+
+ test("should handle file input callback", () => {
+ const mockCallback = jest.fn();
+ const mockFile = new File(['test: value'], 'test.yml', { type: 'text/yaml' });
+ const mockInput = {
+ type: 'file',
+ accept: '.yaml,.yml',
+ style: { display: 'none' },
+ value: '',
+ files: [mockFile],
+ onchange: null
+ };
+
+ mockWindow.document.createElement.mockReturnValue(mockInput);
+
+ const input = YAMLFileHelper.createFileInput(mockCallback);
+
+ // Simulate file selection
+ input.files = [mockFile];
+ input.onchange();
+
+ expect(mockCallback).toHaveBeenCalledWith(mockFile);
+ expect(input.value).toBe('');
+ });
+
+ test("should import from YAML file", async () => {
+ const yamlContent = `
+title: Test Survey
+pages:
+ - name: page1
+ elements:
+ - type: text
+ name: question1
+`;
+
+ const mockFile = new File([yamlContent], 'test.yml', { type: 'text/yaml' });
+
+ // Mock FileReader
+ const mockFileReader = {
+ onload: null,
+ onerror: null,
+ readAsText: jest.fn(),
+ result: yamlContent
+ };
+
+ global.FileReader = jest.fn(() => mockFileReader);
+
+ const promise = YAMLFileHelper.importFromYAMLFile(mockFile);
+
+ // Simulate FileReader onload
+ mockFileReader.onload({ target: { result: yamlContent } });
+
+ const result = await promise;
+
+ expect(result).toBeDefined();
+ expect(result.title).toBe("Test Survey");
+ expect(result.pages[0].name).toBe("page1");
+ expect(mockFileReader.readAsText).toHaveBeenCalledWith(mockFile);
+ });
+
+ test("should handle import file read error", async () => {
+ const mockFile = new File(['test: value'], 'test.yml', { type: 'text/yaml' });
+
+ const mockFileReader = {
+ onload: null,
+ onerror: null,
+ readAsText: jest.fn()
+ };
+
+ global.FileReader = jest.fn(() => mockFileReader);
+
+ const promise = YAMLFileHelper.importFromYAMLFile(mockFile);
+
+ // Simulate FileReader error
+ mockFileReader.onerror();
+
+ await expect(promise).rejects.toThrow("Failed to read file");
+ });
+
+ test("should handle import YAML parse error", async () => {
+ const invalidYaml = "invalid: yaml: structure:";
+ const mockFile = new File([invalidYaml], 'test.yml', { type: 'text/yaml' });
+
+ const mockFileReader = {
+ onload: null,
+ onerror: null,
+ readAsText: jest.fn()
+ };
+
+ global.FileReader = jest.fn(() => mockFileReader);
+
+ const promise = YAMLFileHelper.importFromYAMLFile(mockFile);
+
+ // Simulate FileReader onload with invalid YAML
+ mockFileReader.onload({ target: { result: invalidYaml } });
+
+ await expect(promise).rejects.toThrow("Failed to parse YAML file");
+ });
+
+ test("should handle missing document for createFileInput", () => {
+ const originalDocument = global.document;
+ delete global.document;
+
+ expect(() => {
+ YAMLFileHelper.createFileInput(() => {});
+ }).toThrow("Document is not available");
+
+ global.document = originalDocument;
+ });
+
+ test("should handle missing window for export", () => {
+ const originalWindow = global.window;
+ delete global.window;
+
+ const testObj = { test: "value" };
+
+ // Should return early without throwing
+ expect(() => {
+ YAMLFileHelper.exportToYAMLFile(testObj, "test.yml");
+ }).not.toThrow();
+
+ global.window = originalWindow;
+ });
+ });
+});
\ No newline at end of file
diff --git a/packages/survey-creator-core/tests/yaml-support.tests.ts b/packages/survey-creator-core/tests/yaml-support.tests.ts
new file mode 100644
index 0000000000..7415e85c69
--- /dev/null
+++ b/packages/survey-creator-core/tests/yaml-support.tests.ts
@@ -0,0 +1,214 @@
+import { SurveyCreatorModel } from "../src/creator-base";
+import { SurveyYAML } from "../src/yaml-utils";
+
+test("SurveyYAML - parse valid YAML", () => {
+ const yamlString = `
+pages:
+ - name: "page1"
+ elements:
+ - type: "text"
+ name: "firstName"
+ title: "First Name"
+ - type: "text"
+ name: "lastName"
+ title: "Last Name"
+`;
+
+ const result = SurveyYAML.parse(yamlString);
+ expect(result).toBeDefined();
+ expect(result.pages).toBeDefined();
+ expect(result.pages.length).toBe(1);
+ expect(result.pages[0].elements.length).toBe(2);
+ expect(result.pages[0].elements[0].name).toBe("firstName");
+});
+
+test("SurveyYAML - stringify object to YAML", () => {
+ const obj = {
+ pages: [{
+ name: "page1",
+ elements: [{
+ type: "text",
+ name: "firstName",
+ title: "First Name"
+ }]
+ }]
+ };
+
+ const yamlString = SurveyYAML.stringify(obj);
+ expect(yamlString).toBeDefined();
+ expect(yamlString).toContain("pages:");
+ expect(yamlString).toContain("name: page1");
+ expect(yamlString).toContain("type: text");
+ expect(yamlString).toContain("name: firstName");
+});
+
+test("SurveyYAML - parse invalid YAML throws error", () => {
+ const invalidYaml = `
+pages:
+ - name: "page1"
+ elements:
+ invalid: yaml: structure:
+`;
+
+ expect(() => SurveyYAML.parse(invalidYaml)).toThrow("YAML parsing error");
+});
+
+test("SurveyYAML - isValidYAML returns correct values", () => {
+ const validYaml = "key: value\narray:\n - item1\n - item2";
+ const invalidYaml = "key: value\n invalid: yaml: structure:";
+
+ expect(SurveyYAML.isValidYAML(validYaml)).toBe(true);
+ expect(SurveyYAML.isValidYAML(invalidYaml)).toBe(false);
+});
+
+test("SurveyYAML - jsonToYaml conversion", () => {
+ const jsonString = JSON.stringify({
+ pages: [{
+ name: "page1",
+ elements: [{
+ type: "text",
+ name: "firstName"
+ }]
+ }]
+ });
+
+ const yamlString = SurveyYAML.jsonToYaml(jsonString);
+ expect(yamlString).toContain("pages:");
+ expect(yamlString).toContain("name: page1");
+ expect(yamlString).toContain("type: text");
+});
+
+test("SurveyYAML - yamlToJson conversion", () => {
+ const yamlString = `
+pages:
+ - name: page1
+ elements:
+ - type: text
+ name: firstName
+`;
+
+ const jsonString = SurveyYAML.yamlToJson(yamlString);
+ const obj = JSON.parse(jsonString);
+ expect(obj.pages).toBeDefined();
+ expect(obj.pages[0].name).toBe("page1");
+ expect(obj.pages[0].elements[0].type).toBe("text");
+});
+
+test("SurveyCreator - yamlText property getter", () => {
+ const creator = new SurveyCreatorModel({});
+
+ // Set a simple survey
+ creator.JSON = {
+ pages: [{
+ name: "page1",
+ elements: [{
+ type: "text",
+ name: "firstName",
+ title: "First Name"
+ }]
+ }]
+ };
+
+ const yamlText = creator.yamlText;
+ expect(yamlText).toBeDefined();
+ expect(yamlText).toContain("pages:");
+ expect(yamlText).toContain("name: page1");
+ expect(yamlText).toContain("type: text");
+ expect(yamlText).toContain("name: firstName");
+});
+
+test("SurveyCreator - yamlText property setter", () => {
+ const creator = new SurveyCreatorModel({});
+
+ const yamlString = `
+pages:
+ - name: testPage
+ elements:
+ - type: text
+ name: testQuestion
+ title: Test Question
+`;
+
+ creator.yamlText = yamlString;
+
+ const json = creator.JSON;
+ expect(json.pages).toBeDefined();
+ expect(json.pages.length).toBe(1);
+ expect(json.pages[0].name).toBe("testPage");
+ expect(json.pages[0].elements.length).toBe(1);
+ expect(json.pages[0].elements[0].name).toBe("testQuestion");
+ expect(json.pages[0].elements[0].title).toBe("Test Question");
+});
+
+test("SurveyCreator - toYAML method", () => {
+ const creator = new SurveyCreatorModel({});
+
+ creator.JSON = {
+ pages: [{
+ name: "page1",
+ elements: [{
+ type: "text",
+ name: "firstName"
+ }]
+ }]
+ };
+
+ const yamlString = creator.toYAML();
+ expect(yamlString).toBeDefined();
+ expect(yamlString).toContain("pages:");
+ expect(yamlString).toContain("name: page1");
+});
+
+test("SurveyCreator - fromYAML method", () => {
+ const creator = new SurveyCreatorModel({});
+
+ const yamlString = `
+pages:
+ - name: testPage
+ elements:
+ - type: text
+ name: testQuestion
+`;
+
+ creator.fromYAML(yamlString);
+
+ const json = creator.JSON;
+ expect(json.pages[0].name).toBe("testPage");
+ expect(json.pages[0].elements[0].name).toBe("testQuestion");
+});
+
+test("SurveyCreator - yamlText getter/setter round trip", () => {
+ const creator = new SurveyCreatorModel({});
+
+ const originalYaml = `
+pages:
+ - name: testPage
+ elements:
+ - type: text
+ name: testQuestion
+ title: Test Question
+ - type: dropdown
+ name: testDropdown
+ title: Test Dropdown
+ choices:
+ - value: option1
+ text: Option 1
+ - value: option2
+ text: Option 2
+`;
+
+ // Set YAML
+ creator.yamlText = originalYaml;
+
+ // Get YAML back
+ const retrievedYaml = creator.yamlText;
+
+ // Parse both to objects for comparison (since formatting might differ)
+ const originalObj = SurveyYAML.parse(originalYaml);
+ const retrievedObj = SurveyYAML.parse(retrievedYaml);
+
+ expect(retrievedObj.pages[0].name).toBe(originalObj.pages[0].name);
+ expect(retrievedObj.pages[0].elements.length).toBe(originalObj.pages[0].elements.length);
+ expect(retrievedObj.pages[0].elements[0].name).toBe(originalObj.pages[0].elements[0].name);
+ expect(retrievedObj.pages[0].elements[1].choices.length).toBe(originalObj.pages[0].elements[1].choices.length);
+});
\ No newline at end of file
diff --git a/packages/survey-creator-core/tests/yaml-utils.tests.ts b/packages/survey-creator-core/tests/yaml-utils.tests.ts
new file mode 100644
index 0000000000..f614608a94
--- /dev/null
+++ b/packages/survey-creator-core/tests/yaml-utils.tests.ts
@@ -0,0 +1,274 @@
+import { SurveyYAML } from "../src/yaml-utils";
+
+describe("SurveyYAML Utility Tests", () => {
+ test("should parse valid YAML string", () => {
+ const yamlString = `
+name: testSurvey
+pages:
+ - name: page1
+ elements:
+ - type: text
+ name: firstName
+ title: First Name
+`;
+
+ const result = SurveyYAML.parse(yamlString);
+ expect(result).toBeDefined();
+ expect(result.name).toBe("testSurvey");
+ expect(result.pages).toBeDefined();
+ expect(Array.isArray(result.pages)).toBe(true);
+ expect(result.pages.length).toBe(1);
+ expect(result.pages[0].name).toBe("page1");
+ expect(result.pages[0].elements.length).toBe(1);
+ expect(result.pages[0].elements[0].type).toBe("text");
+ expect(result.pages[0].elements[0].name).toBe("firstName");
+ });
+
+ test("should stringify object to YAML", () => {
+ const obj = {
+ name: "testSurvey",
+ pages: [{
+ name: "page1",
+ elements: [{
+ type: "text",
+ name: "firstName",
+ title: "First Name"
+ }, {
+ type: "dropdown",
+ name: "country",
+ title: "Select Country",
+ choices: [
+ { value: "us", text: "United States" },
+ { value: "ca", text: "Canada" }
+ ]
+ }]
+ }]
+ };
+
+ const yamlString = SurveyYAML.stringify(obj);
+ expect(yamlString).toBeDefined();
+ expect(typeof yamlString).toBe("string");
+ expect(yamlString).toContain("name: testSurvey");
+ expect(yamlString).toContain("pages:");
+ expect(yamlString).toContain("- name: page1");
+ expect(yamlString).toContain("type: text");
+ expect(yamlString).toContain("name: firstName");
+ expect(yamlString).toContain("type: dropdown");
+ expect(yamlString).toContain("choices:");
+ });
+
+ test("should throw error for invalid YAML", () => {
+ const invalidYaml = `
+pages:
+ - name: page1
+ elements:
+ invalid: yaml: structure:
+ more: invalid: syntax
+`;
+
+ expect(() => SurveyYAML.parse(invalidYaml)).toThrow("YAML parsing error");
+ });
+
+ test("should validate YAML correctly", () => {
+ const validYaml = "name: test\nvalue: 123\narray:\n - item1\n - item2";
+ const invalidYaml = "name: test\ninvalid: yaml: structure:";
+
+ expect(SurveyYAML.isValidYAML(validYaml)).toBe(true);
+ expect(SurveyYAML.isValidYAML(invalidYaml)).toBe(false);
+ expect(SurveyYAML.isValidYAML("")).toBe(true); // Empty string is valid YAML
+ });
+
+ test("should convert JSON string to YAML", () => {
+ const jsonString = JSON.stringify({
+ pages: [{
+ name: "page1",
+ elements: [{
+ type: "text",
+ name: "firstName"
+ }]
+ }]
+ });
+
+ const yamlString = SurveyYAML.jsonToYaml(jsonString);
+ expect(yamlString).toBeDefined();
+ expect(yamlString).toContain("pages:");
+ expect(yamlString).toContain("name: page1");
+ expect(yamlString).toContain("type: text");
+ });
+
+ test("should convert YAML string to JSON", () => {
+ const yamlString = `
+pages:
+ - name: page1
+ elements:
+ - type: text
+ name: firstName
+ - type: rating
+ name: satisfaction
+ rateMin: 1
+ rateMax: 5
+`;
+
+ const jsonString = SurveyYAML.yamlToJson(yamlString);
+ const obj = JSON.parse(jsonString);
+ expect(obj.pages).toBeDefined();
+ expect(obj.pages[0].name).toBe("page1");
+ expect(obj.pages[0].elements[0].type).toBe("text");
+ expect(obj.pages[0].elements[1].rateMin).toBe(1);
+ expect(obj.pages[0].elements[1].rateMax).toBe(5);
+ });
+
+ test("should handle round-trip conversion", () => {
+ const originalObj = {
+ title: "Customer Satisfaction Survey",
+ pages: [{
+ name: "page1",
+ elements: [{
+ type: "text",
+ name: "firstName",
+ title: "First Name",
+ isRequired: true
+ }, {
+ type: "dropdown",
+ name: "country",
+ title: "Country",
+ choices: [
+ { value: "us", text: "United States" },
+ { value: "ca", text: "Canada" },
+ { value: "uk", text: "United Kingdom" }
+ ]
+ }, {
+ type: "rating",
+ name: "satisfaction",
+ title: "Overall Satisfaction",
+ rateMin: 1,
+ rateMax: 10,
+ rateStep: 1
+ }]
+ }]
+ };
+
+ // Convert to YAML and back
+ const yamlString = SurveyYAML.stringify(originalObj);
+ const roundTripObj = SurveyYAML.parse(yamlString);
+
+ // Compare key properties
+ expect(roundTripObj.title).toBe(originalObj.title);
+ expect(roundTripObj.pages.length).toBe(originalObj.pages.length);
+ expect(roundTripObj.pages[0].name).toBe(originalObj.pages[0].name);
+ expect(roundTripObj.pages[0].elements.length).toBe(originalObj.pages[0].elements.length);
+
+ // Check each element
+ const originalElements = originalObj.pages[0].elements;
+ const roundTripElements = roundTripObj.pages[0].elements;
+
+ for (let i = 0; i < originalElements.length; i++) {
+ expect(roundTripElements[i].type).toBe(originalElements[i].type);
+ expect(roundTripElements[i].name).toBe(originalElements[i].name);
+ expect(roundTripElements[i].title).toBe(originalElements[i].title);
+ }
+ });
+
+ test("should handle special characters and Unicode", () => {
+ const obj = {
+ title: "Survey with Special Characters: !@#$%^&*()",
+ description: "Unicode test: 你好世界 🌍 こんにちは",
+ pages: [{
+ name: "page1",
+ elements: [{
+ type: "text",
+ name: "email",
+ title: "Email Address (user@domain.com)",
+ placeholder: "Enter your email..."
+ }]
+ }]
+ };
+
+ const yamlString = SurveyYAML.stringify(obj);
+ const parsedObj = SurveyYAML.parse(yamlString);
+
+ expect(parsedObj.title).toBe(obj.title);
+ expect(parsedObj.description).toBe(obj.description);
+ expect(parsedObj.pages[0].elements[0].placeholder).toBe(obj.pages[0].elements[0].placeholder);
+ });
+
+ test("should handle nested objects and arrays", () => {
+ const complexObj = {
+ pages: [{
+ name: "page1",
+ elements: [{
+ type: "matrixdynamic",
+ name: "matrix",
+ title: "Matrix Question",
+ columns: [
+ { name: "col1", title: "Column 1", cellType: "text" },
+ { name: "col2", title: "Column 2", cellType: "dropdown",
+ choices: ["Option 1", "Option 2"] }
+ ],
+ choices: [1, 2, 3, 4, 5],
+ cellType: "text",
+ confirmDelete: true
+ }]
+ }],
+ triggers: [{
+ type: "complete",
+ expression: "{question1} = 'yes'"
+ }],
+ calculatedValues: [{
+ name: "var1",
+ expression: "{question1} + {question2}"
+ }]
+ };
+
+ const yamlString = SurveyYAML.stringify(complexObj);
+ const parsedObj = SurveyYAML.parse(yamlString);
+
+ expect(parsedObj.pages[0].elements[0].columns.length).toBe(2);
+ expect(parsedObj.pages[0].elements[0].columns[0].cellType).toBe("text");
+ expect(parsedObj.pages[0].elements[0].columns[1].choices.length).toBe(2);
+ expect(parsedObj.triggers.length).toBe(1);
+ expect(parsedObj.calculatedValues.length).toBe(1);
+ });
+
+ test("should handle custom YAML options", () => {
+ const obj = {
+ title: "Test Survey",
+ pages: [{ name: "page1", elements: [] }]
+ };
+
+ const customOptions = {
+ indent: 4,
+ lineWidth: 80,
+ sortKeys: true
+ };
+
+ const yamlString = SurveyYAML.stringify(obj, customOptions);
+ expect(yamlString).toBeDefined();
+ expect(typeof yamlString).toBe("string");
+
+ // Verify we can still parse it back
+ const parsedObj = SurveyYAML.parse(yamlString);
+ expect(parsedObj.title).toBe(obj.title);
+ });
+
+ test("should handle empty and null values", () => {
+ const obj = {
+ title: "",
+ description: null,
+ pages: [],
+ variables: {},
+ showTitle: false,
+ maxValue: 0
+ };
+
+ const yamlString = SurveyYAML.stringify(obj);
+ const parsedObj = SurveyYAML.parse(yamlString);
+
+ expect(parsedObj.title).toBe("");
+ expect(parsedObj.description).toBe(null);
+ expect(Array.isArray(parsedObj.pages)).toBe(true);
+ expect(parsedObj.pages.length).toBe(0);
+ expect(parsedObj.showTitle).toBe(false);
+ expect(parsedObj.maxValue).toBe(0);
+ });
+});
\ No newline at end of file