diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..f8960e1 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,31 @@ +# Build output +build/ +dist/ +.svelte-kit/ +node_modules/ + +# Test reports +playwright-report/ +test-results/ +coverage/ + +# Config and generated files +*.config.js +*.config.ts +.DS_Store +*.min.js + +# Temporary files +*.tmp +*.log +.env +.env.* +!.env.example + +# IDE +.vscode/ +.idea/ + +# Documentation +CLAUDE.md +*.md \ No newline at end of file diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index f4ad724..ace96c9 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -54,4 +54,4 @@ jobs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha - cache-to: type=gha,mode=max \ No newline at end of file + cache-to: type=gha,mode=max diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index b56901a..9914f67 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -9,7 +9,7 @@ on: jobs: lint-and-format: runs-on: ubuntu-latest - + steps: - name: Checkout repository uses: actions/checkout@v4 @@ -30,4 +30,4 @@ jobs: run: npm run format:check - name: Type check - run: npm run check \ No newline at end of file + run: npm run check diff --git a/README-ko.md b/README-ko.md index 06e2ca2..93f9c00 100644 --- a/README-ko.md +++ b/README-ko.md @@ -1,6 +1,6 @@ # PDJsonEditor -*[English](README.md)* +_[English](README.md)_ SvelteKit과 Svelte 5로 구축된 강력한 JSON 시각화 및 편집 도구입니다. JSON 데이터를 코드 에디터와 인터랙티브 그래프 뷰에서 동시에 보고 편집할 수 있습니다. @@ -11,18 +11,21 @@ SvelteKit과 Svelte 5로 구축된 강력한 JSON 시각화 및 편집 도구입 ## ✨ 주요 기능 ### 📝 고급 JSON 에디터 + - **문법 강조**: CodeMirror 기반 JSON 문법 강조 기능 - **실시간 검증**: 즉시 JSON 문법 검증 및 오류 리포팅 - **포맷 & 압축**: 원클릭 JSON 포맷팅 및 압축 기능 - **네비게이션**: 그래프 노드 클릭으로 해당 JSON 위치로 이동 ### 🔗 HTTP 요청 통합 + - **다중 메소드 지원**: GET, POST, PUT, DELETE, PATCH 요청 - **커스텀 헤더**: HTTP 헤더 추가 및 관리 - **요청 본문**: POST/PUT/PATCH용 커스텀 요청 본문 설정 - **URL 가져오기**: URL에서 직접 JSON 데이터 가져오기 ### 📊 인터랙티브 그래프 시각화 + - **트리 구조**: JSON을 인터랙티브 트리 그래프로 시각화 - **컴팩트 노드**: 원시 값 그룹화 디스플레이 - **확장/축소**: 시각적 표시와 함께 노드 확장 토글 @@ -30,12 +33,14 @@ SvelteKit과 Svelte 5로 구축된 강력한 JSON 시각화 및 편집 도구입 - **자동 레이아웃**: Dagre 기반 자동 그래프 레이아웃 ### 🎯 스마트 노드 디스플레이 + - **그룹화된 원시 값**: 명확성을 위해 부모 노드에서 원시 값 그룹화 - **참조 타입**: 객체와 배열을 참조로 표시 (예: `address {3}`, `hobbies [3]`) - **더 보기**: 20개 이상의 항목을 가진 노드를 "더 보기" 기능과 함께 자동 축소 - **개별 토글**: 개별 참조 항목 확장/축소 ### 🌐 다국어 지원 + - **다중 언어**: 영어 및 한국어 지원 - **언어 전환기**: 헤더에서 쉬운 언어 전환 - **지속적 설정**: localStorage에 언어 설정 저장 @@ -43,28 +48,33 @@ SvelteKit과 Svelte 5로 구축된 강력한 JSON 시각화 및 편집 도구입 ## 🚀 시작하기 ### 필수 조건 + - Node.js v20.19 이상 - npm 또는 yarn 패키지 매니저 ### 설치 1. **저장소 클론** + ```bash git clone https://github.com/podosoft-dev/pdjsoneditor.git cd pdjsoneditor ``` 2. **의존성 설치** + ```bash npm install ``` 3. **개발 서버 시작** + ```bash npm run dev ``` 4. **브라우저에서 열기** + ``` http://localhost:5173 ``` @@ -141,11 +151,12 @@ services: image: ghcr.io/podosoft-dev/pdjsoneditor:latest container_name: pdjsoneditor ports: - - "3000:3000" + - '3000:3000' restart: unless-stopped ``` 그 다음 실행: + ```bash docker-compose up -d ``` @@ -155,17 +166,20 @@ docker-compose up -d ## 📖 사용 방법 ### 기본 JSON 편집 + 1. **JSON 붙여넣기 또는 입력**: 왼쪽 에디터 패널에서 2. **구조 보기**: 오른쪽 그래프 패널에서 3. **뷰 간 네비게이션**: 노드 클릭 또는 에디터 사용 ### URL에서 데이터 가져오기 + 1. **HTTP 메소드 선택**: 드롭다운에서 (GET, POST, PUT, DELETE, PATCH) 2. **URL 입력**: 입력 필드에 3. **헤더와 본문 설정**: Settings 버튼 사용 (선택사항) 4. **"Go" 클릭**: JSON 데이터를 가져와서 로드 ### 그래프 상호작용 + - **확장/축소**: 노드의 색깔있는 핸들 클릭 - **더 보기**: 많은 항목이 있는 노드에서 "더 보기" 클릭 - **네비게이션**: 노드 클릭으로 해당 JSON 위치로 이동 @@ -193,4 +207,4 @@ docker-compose up -d
PODOSOFT에서 ❤️로 만들었습니다 -
\ No newline at end of file + diff --git a/README.md b/README.md index ef893df..6960418 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # PDJsonEditor -*[한국어](README-ko.md)* +_[한국어](README-ko.md)_ A powerful JSON visualization and editing tool built with SvelteKit and Svelte 5. View and edit JSON data simultaneously in both code editor and interactive graph views. @@ -11,18 +11,21 @@ A powerful JSON visualization and editing tool built with SvelteKit and Svelte 5 ## ✨ Features ### 📝 Advanced JSON Editor + - **Syntax Highlighting**: CodeMirror-powered editor with JSON syntax highlighting - **Real-time Validation**: Instant JSON syntax validation and error reporting - **Format & Minify**: One-click JSON formatting and minification - **Navigation**: Click on graph nodes to jump to corresponding JSON location ### 🔗 HTTP Request Integration + - **Multi-Method Support**: GET, POST, PUT, DELETE, PATCH requests - **Custom Headers**: Add and manage HTTP headers - **Request Body**: Configure custom request bodies for POST/PUT/PATCH - **URL Import**: Fetch JSON data directly from URLs ### 📊 Interactive Graph Visualization + - **Tree Structure**: Visualize JSON as an interactive tree graph - **Compact Nodes**: Compact display grouping primitive values - **Expand/Collapse**: Toggle node expansion with visual indicators @@ -30,12 +33,14 @@ A powerful JSON visualization and editing tool built with SvelteKit and Svelte 5 - **Auto Layout**: Dagre-powered automatic graph layout ### 🎯 Smart Node Display + - **Grouped Primitives**: Primitive values grouped in parent nodes for clarity - **Reference Types**: Objects and arrays shown as references (e.g., `address {3}`, `hobbies [3]`) - **Show More**: Automatically collapse nodes with 20+ items with "show more" functionality - **Individual Toggles**: Expand/collapse individual reference items ### 🌐 Internationalization + - **Multi-language**: English and Korean support - **Language Switcher**: Easy language switching in header - **Persistent Settings**: Language preference saved in localStorage @@ -43,28 +48,33 @@ A powerful JSON visualization and editing tool built with SvelteKit and Svelte 5 ## 🚀 Getting Started ### Prerequisites + - Node.js v20.19 or higher - npm or yarn package manager ### Installation 1. **Clone the repository** + ```bash git clone https://github.com/podosoft-dev/pdjsoneditor.git cd pdjsoneditor ``` 2. **Install dependencies** + ```bash npm install ``` 3. **Start development server** + ```bash npm run dev ``` 4. **Open in browser** + ``` http://localhost:5173 ``` @@ -141,11 +151,12 @@ services: image: ghcr.io/podosoft-dev/pdjsoneditor:latest container_name: pdjsoneditor ports: - - "3000:3000" + - '3000:3000' restart: unless-stopped ``` Then run: + ```bash docker-compose up -d ``` @@ -155,17 +166,20 @@ Access the application at `http://localhost:3000` ## 📖 Usage ### Basic JSON Editing + 1. **Paste or type JSON** in the left editor panel 2. **View the structure** in the right graph panel 3. **Navigate between views** by clicking nodes or using the editor ### Fetching Data from URLs + 1. **Select HTTP method** from the dropdown (GET, POST, PUT, DELETE, PATCH) 2. **Enter the URL** in the input field 3. **Configure headers and body** using the Settings button (optional) 4. **Click "Go"** to fetch and load the JSON data ### Graph Interaction + - **Expand/Collapse**: Click the colored handles on nodes - **Show More**: Click "Show more" on nodes with many items - **Navigate**: Click nodes to jump to corresponding JSON location @@ -193,4 +207,4 @@ If you encounter any issues or have questions, please [open an issue](https://gi
Made with ❤️ by PODOSOFT -
\ No newline at end of file + diff --git a/docker-compose.yml b/docker-compose.yml index 73b546e..09762e6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,15 +8,15 @@ services: image: pdjsoneditor:latest container_name: pdjsoneditor ports: - - "3000:3000" + - '3000:3000' environment: - NODE_ENV=production - HOST=0.0.0.0 - PORT=3000 restart: unless-stopped healthcheck: - test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000"] + test: ['CMD', 'wget', '--no-verbose', '--tries=1', '--spider', 'http://localhost:3000'] interval: 30s timeout: 10s retries: 3 - start_period: 40s \ No newline at end of file + start_period: 40s diff --git a/playwright.config.ts b/playwright.config.ts index d2f8557..1ef6ec3 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -22,10 +22,7 @@ export default defineConfig({ /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: [ - ['html', { outputFolder: 'playwright-report' }], - ['list'] - ], + reporter: [['html', { outputFolder: 'playwright-report' }], ['list']], /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ @@ -33,25 +30,25 @@ export default defineConfig({ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry', /* Take screenshot on failure */ - screenshot: 'only-on-failure', + screenshot: 'only-on-failure' }, /* Configure projects for major browsers */ projects: [ { name: 'chromium', - use: { ...devices['Desktop Chrome'] }, + use: { ...devices['Desktop Chrome'] } }, { name: 'firefox', - use: { ...devices['Desktop Firefox'] }, + use: { ...devices['Desktop Firefox'] } }, { name: 'webkit', - use: { ...devices['Desktop Safari'] }, - }, + use: { ...devices['Desktop Safari'] } + } /* Test against mobile viewports. */ // { @@ -80,13 +77,13 @@ export default defineConfig({ command: 'npm run dev', url: 'http://localhost:5173', reuseExistingServer: !process.env.CI, - timeout: 120 * 1000, + timeout: 120 * 1000 }, { command: 'npm run test:server', url: 'http://localhost:3001', reuseExistingServer: !process.env.CI, - timeout: 120 * 1000, + timeout: 120 * 1000 } - ], -}); \ No newline at end of file + ] +}); diff --git a/src/i18n/en/index.ts b/src/i18n/en/index.ts index df875fc..dc96a3b 100644 --- a/src/i18n/en/index.ts +++ b/src/i18n/en/index.ts @@ -30,9 +30,10 @@ const en: BaseTranslation = { bodyDescription: 'Configure the request body for POST/PUT/PATCH requests.', bodyPlaceholder: 'Enter request body (JSON, XML, etc.)', useEditorContent: 'Use editor content as request body', - sendAsRawText: 'Send as raw text (don\'t parse as JSON)', + sendAsRawText: "Send as raw text (don't parse as JSON)", clearAll: 'Clear All', - clearAllConfirm: 'Are you sure you want to clear all settings? This will remove all saved headers, body, and URL from storage.', + clearAllConfirm: + 'Are you sure you want to clear all settings? This will remove all saved headers, body, and URL from storage.', cancel: 'Cancel', save: 'Save' }, diff --git a/src/i18n/i18n-svelte.ts b/src/i18n/i18n-svelte.ts index 6cdffb3..2d66bc9 100644 --- a/src/i18n/i18n-svelte.ts +++ b/src/i18n/i18n-svelte.ts @@ -1,12 +1,17 @@ // This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten. /* eslint-disable */ -import { initI18nSvelte } from 'typesafe-i18n/svelte' -import type { Formatters, Locales, TranslationFunctions, Translations } from './i18n-types' -import { loadedFormatters, loadedLocales } from './i18n-util' +import { initI18nSvelte } from 'typesafe-i18n/svelte'; +import type { Formatters, Locales, TranslationFunctions, Translations } from './i18n-types'; +import { loadedFormatters, loadedLocales } from './i18n-util'; -const { locale, LL, setLocale } = initI18nSvelte(loadedLocales, loadedFormatters) +const { locale, LL, setLocale } = initI18nSvelte< + Locales, + Translations, + TranslationFunctions, + Formatters +>(loadedLocales, loadedFormatters); -export { locale, LL, setLocale } +export { locale, LL, setLocale }; -export default LL +export default LL; diff --git a/src/i18n/i18n-types.ts b/src/i18n/i18n-types.ts index 60a437c..702cb16 100644 --- a/src/i18n/i18n-types.ts +++ b/src/i18n/i18n-types.ts @@ -1,363 +1,365 @@ // This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten. /* eslint-disable */ -import type { BaseTranslation as BaseTranslationType, LocalizedString, RequiredParams } from 'typesafe-i18n' +import type { + BaseTranslation as BaseTranslationType, + LocalizedString, + RequiredParams +} from 'typesafe-i18n'; -export type BaseTranslation = BaseTranslationType -export type BaseLocale = 'en' +export type BaseTranslation = BaseTranslationType; +export type BaseLocale = 'en'; -export type Locales = - | 'en' - | 'ko' +export type Locales = 'en' | 'ko'; -export type Translation = RootTranslation +export type Translation = RootTranslation; -export type Translations = RootTranslation +export type Translations = RootTranslation; type RootTranslation = { header: { /** * J​S​O​N​ ​E​d​i​t​o​r */ - title: string + title: string; /** * C​l​e​a​r */ - clear: string + clear: string; /** * C​o​p​y */ - copy: string + copy: string; /** * C​o​p​i​e​d​ ​t​o​ ​c​l​i​p​b​o​a​r​d */ - copySuccess: string + copySuccess: string; /** * F​a​i​l​e​d​ ​t​o​ ​c​o​p​y​ ​t​o​ ​c​l​i​p​b​o​a​r​d */ - copyError: string + copyError: string; /** * F​o​r​m​a​t */ - format: string + format: string; /** * M​i​n​i​f​y */ - minify: string + minify: string; /** * S​a​m​p​l​e​ ​D​a​t​a */ - sample: string + sample: string; /** * L​a​n​g​u​a​g​e */ - language: string - } + language: string; + }; editor: { /** * E​n​t​e​r​ ​y​o​u​r​ ​J​S​O​N​ ​d​a​t​a​ ​h​e​r​e​.​.​. */ - placeholder: string + placeholder: string; /** * I​n​v​a​l​i​d​ ​J​S​O​N */ - invalidJson: string + invalidJson: string; /** * V​a​l​i​d​ ​J​S​O​N */ - validJson: string + validJson: string; /** * E​n​t​e​r​ ​U​R​L​ ​t​o​ ​f​e​t​c​h​ ​J​S​O​N​.​.​. */ - urlPlaceholder: string + urlPlaceholder: string; /** * U​R​L​ ​i​s​ ​r​e​q​u​i​r​e​d */ - urlRequired: string + urlRequired: string; /** * F​a​i​l​e​d​ ​t​o​ ​f​e​t​c​h​ ​d​a​t​a */ - fetchError: string + fetchError: string; /** * G​o */ - go: string + go: string; /** * R​e​q​u​e​s​t​ ​S​e​t​t​i​n​g​s */ - requestSettings: string + requestSettings: string; /** * C​o​n​f​i​g​u​r​e​ ​H​T​T​P​ ​h​e​a​d​e​r​s​ ​a​n​d​ ​r​e​q​u​e​s​t​ ​b​o​d​y */ - requestSettingsDescription: string + requestSettingsDescription: string; /** * H​e​a​d​e​r​s */ - headers: string + headers: string; /** * H​e​a​d​e​r​ ​k​e​y */ - headerKey: string + headerKey: string; /** * H​e​a​d​e​r​ ​v​a​l​u​e */ - headerValue: string + headerValue: string; /** * A​d​d​ ​H​e​a​d​e​r */ - addHeader: string + addHeader: string; /** * B​o​d​y */ - body: string + body: string; /** * C​o​n​f​i​g​u​r​e​ ​t​h​e​ ​r​e​q​u​e​s​t​ ​b​o​d​y​ ​f​o​r​ ​P​O​S​T​/​P​U​T​/​P​A​T​C​H​ ​r​e​q​u​e​s​t​s​. */ - bodyDescription: string + bodyDescription: string; /** * E​n​t​e​r​ ​r​e​q​u​e​s​t​ ​b​o​d​y​ ​(​J​S​O​N​,​ ​X​M​L​,​ ​e​t​c​.​) */ - bodyPlaceholder: string + bodyPlaceholder: string; /** * U​s​e​ ​e​d​i​t​o​r​ ​c​o​n​t​e​n​t​ ​a​s​ ​r​e​q​u​e​s​t​ ​b​o​d​y */ - useEditorContent: string + useEditorContent: string; /** * S​e​n​d​ ​a​s​ ​r​a​w​ ​t​e​x​t​ ​(​d​o​n​'​t​ ​p​a​r​s​e​ ​a​s​ ​J​S​O​N​) */ - sendAsRawText: string + sendAsRawText: string; /** * C​l​e​a​r​ ​A​l​l */ - clearAll: string + clearAll: string; /** * A​r​e​ ​y​o​u​ ​s​u​r​e​ ​y​o​u​ ​w​a​n​t​ ​t​o​ ​c​l​e​a​r​ ​a​l​l​ ​s​e​t​t​i​n​g​s​?​ ​T​h​i​s​ ​w​i​l​l​ ​r​e​m​o​v​e​ ​a​l​l​ ​s​a​v​e​d​ ​h​e​a​d​e​r​s​,​ ​b​o​d​y​,​ ​a​n​d​ ​U​R​L​ ​f​r​o​m​ ​s​t​o​r​a​g​e​. */ - clearAllConfirm: string + clearAllConfirm: string; /** * C​a​n​c​e​l */ - cancel: string + cancel: string; /** * S​a​v​e */ - save: string - } + save: string; + }; graph: { /** * S​h​o​w​ ​{​c​o​u​n​t​}​ ​m​o​r​e * @param {unknown} count */ - showMore: RequiredParams<'count'> + showMore: RequiredParams<'count'>; /** * S​h​o​w​ ​l​e​s​s */ - showLess: string + showLess: string; /** * E​x​p​a​n​d */ - expand: string + expand: string; /** * C​o​l​l​a​p​s​e */ - collapse: string + collapse: string; /** * E​x​p​a​n​d​ ​a​l​l */ - expandAll: string - } + expandAll: string; + }; languages: { /** * E​n​g​l​i​s​h */ - en: string + en: string; /** * 한​국​어 */ - ko: string + ko: string; /** * 日​本​語 */ - ja: string - } + ja: string; + }; footer: { /** * R​e​a​d​y */ - ready: string - } -} + ready: string; + }; +}; export type TranslationFunctions = { header: { /** * JSON Editor */ - title: () => LocalizedString + title: () => LocalizedString; /** * Clear */ - clear: () => LocalizedString + clear: () => LocalizedString; /** * Copy */ - copy: () => LocalizedString + copy: () => LocalizedString; /** * Copied to clipboard */ - copySuccess: () => LocalizedString + copySuccess: () => LocalizedString; /** * Failed to copy to clipboard */ - copyError: () => LocalizedString + copyError: () => LocalizedString; /** * Format */ - format: () => LocalizedString + format: () => LocalizedString; /** * Minify */ - minify: () => LocalizedString + minify: () => LocalizedString; /** * Sample Data */ - sample: () => LocalizedString + sample: () => LocalizedString; /** * Language */ - language: () => LocalizedString - } + language: () => LocalizedString; + }; editor: { /** * Enter your JSON data here... */ - placeholder: () => LocalizedString + placeholder: () => LocalizedString; /** * Invalid JSON */ - invalidJson: () => LocalizedString + invalidJson: () => LocalizedString; /** * Valid JSON */ - validJson: () => LocalizedString + validJson: () => LocalizedString; /** * Enter URL to fetch JSON... */ - urlPlaceholder: () => LocalizedString + urlPlaceholder: () => LocalizedString; /** * URL is required */ - urlRequired: () => LocalizedString + urlRequired: () => LocalizedString; /** * Failed to fetch data */ - fetchError: () => LocalizedString + fetchError: () => LocalizedString; /** * Go */ - go: () => LocalizedString + go: () => LocalizedString; /** * Request Settings */ - requestSettings: () => LocalizedString + requestSettings: () => LocalizedString; /** * Configure HTTP headers and request body */ - requestSettingsDescription: () => LocalizedString + requestSettingsDescription: () => LocalizedString; /** * Headers */ - headers: () => LocalizedString + headers: () => LocalizedString; /** * Header key */ - headerKey: () => LocalizedString + headerKey: () => LocalizedString; /** * Header value */ - headerValue: () => LocalizedString + headerValue: () => LocalizedString; /** * Add Header */ - addHeader: () => LocalizedString + addHeader: () => LocalizedString; /** * Body */ - body: () => LocalizedString + body: () => LocalizedString; /** * Configure the request body for POST/PUT/PATCH requests. */ - bodyDescription: () => LocalizedString + bodyDescription: () => LocalizedString; /** * Enter request body (JSON, XML, etc.) */ - bodyPlaceholder: () => LocalizedString + bodyPlaceholder: () => LocalizedString; /** * Use editor content as request body */ - useEditorContent: () => LocalizedString + useEditorContent: () => LocalizedString; /** * Send as raw text (don't parse as JSON) */ - sendAsRawText: () => LocalizedString + sendAsRawText: () => LocalizedString; /** * Clear All */ - clearAll: () => LocalizedString + clearAll: () => LocalizedString; /** * Are you sure you want to clear all settings? This will remove all saved headers, body, and URL from storage. */ - clearAllConfirm: () => LocalizedString + clearAllConfirm: () => LocalizedString; /** * Cancel */ - cancel: () => LocalizedString + cancel: () => LocalizedString; /** * Save */ - save: () => LocalizedString - } + save: () => LocalizedString; + }; graph: { /** * Show {count} more */ - showMore: (arg: { count: unknown }) => LocalizedString + showMore: (arg: { count: unknown }) => LocalizedString; /** * Show less */ - showLess: () => LocalizedString + showLess: () => LocalizedString; /** * Expand */ - expand: () => LocalizedString + expand: () => LocalizedString; /** * Collapse */ - collapse: () => LocalizedString + collapse: () => LocalizedString; /** * Expand all */ - expandAll: () => LocalizedString - } + expandAll: () => LocalizedString; + }; languages: { /** * English */ - en: () => LocalizedString + en: () => LocalizedString; /** * 한국어 */ - ko: () => LocalizedString + ko: () => LocalizedString; /** * 日本語 */ - ja: () => LocalizedString - } + ja: () => LocalizedString; + }; footer: { /** * Ready */ - ready: () => LocalizedString - } -} + ready: () => LocalizedString; + }; +}; -export type Formatters = {} +export type Formatters = {}; diff --git a/src/i18n/i18n-util.async.ts b/src/i18n/i18n-util.async.ts index 30fd5f7..cb0ba67 100644 --- a/src/i18n/i18n-util.async.ts +++ b/src/i18n/i18n-util.async.ts @@ -1,27 +1,27 @@ // This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten. /* eslint-disable */ -import { initFormatters } from './formatters' -import type { Locales, Translations } from './i18n-types' -import { loadedFormatters, loadedLocales, locales } from './i18n-util' +import { initFormatters } from './formatters'; +import type { Locales, Translations } from './i18n-types'; +import { loadedFormatters, loadedLocales, locales } from './i18n-util'; const localeTranslationLoaders = { en: () => import('./en'), - ko: () => import('./ko'), -} + ko: () => import('./ko') +}; const updateDictionary = (locale: Locales, dictionary: Partial): Translations => - loadedLocales[locale] = { ...loadedLocales[locale], ...dictionary } + (loadedLocales[locale] = { ...loadedLocales[locale], ...dictionary }); export const importLocaleAsync = async (locale: Locales): Promise => - (await localeTranslationLoaders[locale]()).default as unknown as Translations + (await localeTranslationLoaders[locale]()).default as unknown as Translations; export const loadLocaleAsync = async (locale: Locales): Promise => { - updateDictionary(locale, await importLocaleAsync(locale)) - loadFormatters(locale) -} + updateDictionary(locale, await importLocaleAsync(locale)); + loadFormatters(locale); +}; -export const loadAllLocalesAsync = (): Promise => Promise.all(locales.map(loadLocaleAsync)) +export const loadAllLocalesAsync = (): Promise => Promise.all(locales.map(loadLocaleAsync)); export const loadFormatters = (locale: Locales): void => - void (loadedFormatters[locale] = initFormatters(locale)) + void (loadedFormatters[locale] = initFormatters(locale)); diff --git a/src/i18n/i18n-util.sync.ts b/src/i18n/i18n-util.sync.ts index 410289d..bb7a35d 100644 --- a/src/i18n/i18n-util.sync.ts +++ b/src/i18n/i18n-util.sync.ts @@ -1,26 +1,26 @@ // This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten. /* eslint-disable */ -import { initFormatters } from './formatters' -import type { Locales, Translations } from './i18n-types' -import { loadedFormatters, loadedLocales, locales } from './i18n-util' +import { initFormatters } from './formatters'; +import type { Locales, Translations } from './i18n-types'; +import { loadedFormatters, loadedLocales, locales } from './i18n-util'; -import en from './en' -import ko from './ko' +import en from './en'; +import ko from './ko'; const localeTranslations = { en, - ko, -} + ko +}; export const loadLocale = (locale: Locales): void => { - if (loadedLocales[locale]) return + if (loadedLocales[locale]) return; - loadedLocales[locale] = localeTranslations[locale] as unknown as Translations - loadFormatters(locale) -} + loadedLocales[locale] = localeTranslations[locale] as unknown as Translations; + loadFormatters(locale); +}; -export const loadAllLocales = (): void => locales.forEach(loadLocale) +export const loadAllLocales = (): void => locales.forEach(loadLocale); export const loadFormatters = (locale: Locales): void => - void (loadedFormatters[locale] = initFormatters(locale)) + void (loadedFormatters[locale] = initFormatters(locale)); diff --git a/src/i18n/i18n-util.ts b/src/i18n/i18n-util.ts index 1828bff..94e9bff 100644 --- a/src/i18n/i18n-util.ts +++ b/src/i18n/i18n-util.ts @@ -1,38 +1,44 @@ // This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten. /* eslint-disable */ -import { i18n as initI18n, i18nObject as initI18nObject, i18nString as initI18nString } from 'typesafe-i18n' -import type { LocaleDetector } from 'typesafe-i18n/detectors' -import type { LocaleTranslationFunctions, TranslateByString } from 'typesafe-i18n' -import { detectLocale as detectLocaleFn } from 'typesafe-i18n/detectors' -import { initExtendDictionary } from 'typesafe-i18n/utils' -import type { Formatters, Locales, Translations, TranslationFunctions } from './i18n-types' +import { + i18n as initI18n, + i18nObject as initI18nObject, + i18nString as initI18nString +} from 'typesafe-i18n'; +import type { LocaleDetector } from 'typesafe-i18n/detectors'; +import type { LocaleTranslationFunctions, TranslateByString } from 'typesafe-i18n'; +import { detectLocale as detectLocaleFn } from 'typesafe-i18n/detectors'; +import { initExtendDictionary } from 'typesafe-i18n/utils'; +import type { Formatters, Locales, Translations, TranslationFunctions } from './i18n-types'; -export const baseLocale: Locales = 'en' +export const baseLocale: Locales = 'en'; -export const locales: Locales[] = [ - 'en', - 'ko' -] +export const locales: Locales[] = ['en', 'ko']; -export const isLocale = (locale: string): locale is Locales => locales.includes(locale as Locales) +export const isLocale = (locale: string): locale is Locales => locales.includes(locale as Locales); -export const loadedLocales: Record = {} as Record +export const loadedLocales: Record = {} as Record; -export const loadedFormatters: Record = {} as Record +export const loadedFormatters: Record = {} as Record; -export const extendDictionary = initExtendDictionary() +export const extendDictionary = initExtendDictionary(); -export const i18nString = (locale: Locales): TranslateByString => initI18nString(locale, loadedFormatters[locale]) +export const i18nString = (locale: Locales): TranslateByString => + initI18nString(locale, loadedFormatters[locale]); export const i18nObject = (locale: Locales): TranslationFunctions => initI18nObject( locale, loadedLocales[locale], loadedFormatters[locale] - ) + ); export const i18n = (): LocaleTranslationFunctions => - initI18n(loadedLocales, loadedFormatters) + initI18n( + loadedLocales, + loadedFormatters + ); -export const detectLocale = (...detectors: LocaleDetector[]): Locales => detectLocaleFn(baseLocale, locales, ...detectors) +export const detectLocale = (...detectors: LocaleDetector[]): Locales => + detectLocaleFn(baseLocale, locales, ...detectors); diff --git a/src/lib/components/CompactNode.svelte b/src/lib/components/CompactNode.svelte index a2cf915..3c22959 100644 --- a/src/lib/components/CompactNode.svelte +++ b/src/lib/components/CompactNode.svelte @@ -155,7 +155,9 @@ type="target" position={Position.Left} class="!w-1.5 !h-1.5" - style="left: -3px; background-color: {mode.current === 'dark' ? '#6b7280' : '#9ca3af'}; border-color: {mode.current === 'dark' ? '#4b5563' : '#6b7280'};" + style="left: -3px; background-color: {mode.current === 'dark' + ? '#6b7280' + : '#9ca3af'}; border-color: {mode.current === 'dark' ? '#4b5563' : '#6b7280'};" /> {/if} @@ -247,10 +249,18 @@ id={`${id}-${item.key}`} class="!w-2.5 !h-2.5 item-handle" style="position: relative; background-color: {item.isReferenceExpanded - ? (mode.current === 'dark' ? '#10b981' : '#059669') - : (mode.current === 'dark' ? '#6b7280' : '#9ca3af')}; border-color: {item.isReferenceExpanded - ? (mode.current === 'dark' ? '#059669' : '#047857') - : (mode.current === 'dark' ? '#4b5563' : '#6b7280')};" + ? mode.current === 'dark' + ? '#10b981' + : '#059669' + : mode.current === 'dark' + ? '#6b7280' + : '#9ca3af'}; border-color: {item.isReferenceExpanded + ? mode.current === 'dark' + ? '#059669' + : '#047857' + : mode.current === 'dark' + ? '#4b5563' + : '#6b7280'};" /> {/if} @@ -310,7 +320,9 @@ type="source" position={Position.Right} class="!w-1.5 !h-1.5" - style="right: -3px; background-color: {mode.current === 'dark' ? '#6b7280' : '#9ca3af'}; border-color: {mode.current === 'dark' ? '#4b5563' : '#6b7280'};" + style="right: -3px; background-color: {mode.current === 'dark' + ? '#6b7280' + : '#9ca3af'}; border-color: {mode.current === 'dark' ? '#4b5563' : '#6b7280'};" /> {/if} @@ -521,5 +533,4 @@ color: var(--color-foreground); border-color: var(--color-muted-foreground); } - diff --git a/src/lib/components/FitViewController.svelte b/src/lib/components/FitViewController.svelte index 5d9b14d..da4e0bb 100644 --- a/src/lib/components/FitViewController.svelte +++ b/src/lib/components/FitViewController.svelte @@ -1,24 +1,24 @@ - \ No newline at end of file + diff --git a/src/lib/components/JsonEditor.svelte b/src/lib/components/JsonEditor.svelte index 1099158..857af58 100644 --- a/src/lib/components/JsonEditor.svelte +++ b/src/lib/components/JsonEditor.svelte @@ -19,31 +19,31 @@ function buildPathToPositionMap(state: EditorState): Map { const pathMap = new Map(); const tree = syntaxTree(state); - + // Helper to get text content of a node function getNodeText(from: number, to: number): string { return state.doc.sliceString(from, to); } - + // Recursive function to traverse with path context function traverse(cursor: any, path: string[] = []) { do { const nodeName = cursor.name; - + if (nodeName === 'Property') { // Handle object properties let propertyKey = ''; let valueStart = -1; let valueEnd = -1; let valueType = ''; - + if (cursor.firstChild()) { // Get property name if (cursor.name === 'PropertyName') { const keyText = getNodeText(cursor.from, cursor.to); propertyKey = keyText.replace(/^"|"$/g, ''); } - + // Skip to value (past the colon) while (cursor.nextSibling()) { if (cursor.name !== ':') { @@ -53,16 +53,16 @@ break; } } - + // Store the path and position if (propertyKey && valueStart !== -1) { const valuePath = [...path, propertyKey]; const pathStr = valuePath.join('.'); - + if (pathStr) { pathMap.set(pathStr, { start: valueStart, end: valueEnd }); } - + // If value is an array, handle array elements specially if (valueType === 'Array') { if (cursor.firstChild()) { @@ -72,42 +72,47 @@ if (cursor.name === 'Object') { const elementPath = [...valuePath, index.toString()]; const elementPathStr = elementPath.join('.'); - + if (elementPathStr) { pathMap.set(elementPathStr, { start: cursor.from, end: cursor.to }); } - + // Traverse into the object if (cursor.firstChild()) { traverse(cursor, elementPath); cursor.parent(); } - + index++; } else if (cursor.name === 'Array') { const elementPath = [...valuePath, index.toString()]; const elementPathStr = elementPath.join('.'); - + if (elementPathStr) { pathMap.set(elementPathStr, { start: cursor.from, end: cursor.to }); } - + // Recursive array if (cursor.firstChild()) { traverse(cursor, elementPath); cursor.parent(); } - + index++; - } else if (cursor.name !== '[' && cursor.name !== ']' && cursor.name !== ',' && cursor.name !== '⚠') { + } else if ( + cursor.name !== '[' && + cursor.name !== ']' && + cursor.name !== ',' && + cursor.name !== '⚠' + ) { // Primitive values in array const elementPath = [...valuePath, index.toString()]; const elementPathStr = elementPath.join('.'); - + if (elementPathStr) { pathMap.set(elementPathStr, { start: cursor.from, end: cursor.to }); } - + index++; } } while (cursor.nextSibling()); @@ -121,7 +126,7 @@ } } } - + cursor.parent(); } } else if (nodeName === 'Array') { @@ -134,44 +139,49 @@ // For object elements, store the indexed path and traverse const elementPath = [...path, index.toString()]; const pathStr = elementPath.join('.'); - + // Store the position of this array element if (pathStr) { pathMap.set(pathStr, { start: cursor.from, end: cursor.to }); } - + // Traverse into the object to get its properties if (cursor.firstChild()) { traverse(cursor, elementPath); cursor.parent(); } - + index++; } else if (cursor.name === 'Array') { // For nested arrays const elementPath = [...path, index.toString()]; const pathStr = elementPath.join('.'); - + if (pathStr) { pathMap.set(pathStr, { start: cursor.from, end: cursor.to }); } - + // Traverse into the nested array if (cursor.firstChild()) { traverse(cursor, elementPath); cursor.parent(); } - + index++; - } else if (cursor.name !== '[' && cursor.name !== ']' && cursor.name !== ',' && cursor.name !== '⚠') { + } else if ( + cursor.name !== '[' && + cursor.name !== ']' && + cursor.name !== ',' && + cursor.name !== '⚠' + ) { // For primitive values const elementPath = [...path, index.toString()]; const pathStr = elementPath.join('.'); - + if (pathStr) { pathMap.set(pathStr, { start: cursor.from, end: cursor.to }); } - + index++; } } while (cursor.nextSibling()); @@ -198,11 +208,11 @@ } } while (cursor.nextSibling()); } - + // Start traversal const cursor = tree.cursor(); traverse(cursor); - + return pathMap; } @@ -212,10 +222,10 @@ try { // Build the path to position map using syntax tree const pathMap = buildPathToPositionMap(view.state); - + // Look up the position for this path const position = pathMap.get(path); - + if (position) { // Navigate to the found position view.dispatch({ @@ -223,7 +233,9 @@ scrollIntoView: true }); view.focus(); - logger.debug(`[JsonEditor] Navigated to path: ${path} at position ${position.start}-${position.end}`); + logger.debug( + `[JsonEditor] Navigated to path: ${path} at position ${position.start}-${position.end}` + ); } else { logger.warn(`[JsonEditor] Path not found: ${path}`); } diff --git a/src/lib/components/JsonGraph.svelte b/src/lib/components/JsonGraph.svelte index 03c8a31..496d44e 100644 --- a/src/lib/components/JsonGraph.svelte +++ b/src/lib/components/JsonGraph.svelte @@ -19,10 +19,9 @@ import { logger } from '$lib/logger'; import { graphLoading } from '$lib/stores/graphLoading'; // Use Vite worker plugin to ensure bundling works in all environments - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - Vite injects a Worker constructor type via ?worker + // @ts-expect-error - Vite injects a Worker constructor type via ?worker import GraphLayoutWorker from '$lib/workers/graphLayout.worker.ts?worker&module'; - import type { JsonValue, JsonObject, NodeItem, JsonStructure } from '$lib/types/json'; + import type { JsonValue, NodeItem } from '$lib/types/json'; interface Props { jsonData: JsonValue; @@ -94,86 +93,6 @@ return null; } - // Helper function to extract JSON structure (keys only, not values) - function getJsonStructure(obj: JsonValue | null | undefined): JsonStructure | string | null { - if (obj === null || obj === undefined) return null; - if (typeof obj !== 'object') return typeof obj; - - if (Array.isArray(obj)) { - // For arrays, track length and structure of first element - return { - _type: 'array', - _length: obj.length, - _sample: obj.length > 0 ? getJsonStructure(obj[0]) : null - }; - } - - // For objects, track keys and their structures - const structure: JsonStructure = { _type: 'object' }; - for (const key in obj) { - structure[key] = getJsonStructure((obj as JsonObject)[key]); - } - return structure; - } - - // Compare two JSON structures to detect structural changes - function hasStructuralChange( - oldStruct: JsonStructure | string | null, - newStruct: JsonStructure | string | null - ): boolean { - if (oldStruct === newStruct) return false; - if (oldStruct === null || newStruct === null) return true; - if (typeof oldStruct !== typeof newStruct) return true; - - if (typeof oldStruct === 'string') { - return oldStruct !== newStruct; // Type change - } - - if ( - typeof oldStruct === 'object' && - typeof newStruct === 'object' && - oldStruct._type === 'array' && - newStruct._type === 'array' - ) { - // Consider it structural if array length changes significantly (more than 20% or by more than 10 items) - const oldLength = oldStruct._length ?? 0; - const newLength = newStruct._length ?? 0; - const lengthDiff = Math.abs(oldLength - newLength); - const percentChange = lengthDiff / Math.max(oldLength, 1); - if (lengthDiff > 10 || percentChange > 0.2) return true; - - // Check sample structure - return hasStructuralChange(oldStruct._sample ?? null, newStruct._sample ?? null); - } - - if ( - typeof oldStruct === 'object' && - typeof newStruct === 'object' && - oldStruct._type === 'object' && - newStruct._type === 'object' - ) { - // Check if keys are different - const oldKeys = Object.keys(oldStruct).filter((k) => k !== '_type'); - const newKeys = Object.keys(newStruct).filter((k) => k !== '_type'); - - if (oldKeys.length !== newKeys.length) return true; - - for (const key of oldKeys) { - if (!newKeys.includes(key)) return true; - const oldValue = oldStruct[key] ?? null; - const newValue = newStruct[key] ?? null; - if ( - hasStructuralChange( - oldValue as JsonStructure | string | null, - newValue as JsonStructure | string | null - ) - ) - return true; - } - } - - return false; - } const measuredHeights = new Map(); // Store actual measured heights // --- dynamic reflow when heights change --- let reflowScheduled = false; @@ -324,11 +243,6 @@ }); } - // Initial node height (used until actual measurements arrive) - function getInitialNodeHeight(): number { - return LAYOUT_CONFIG.INITIAL_HEIGHT; - } - function estimateDisplayItemCount(node: Node): number { if (!node.data?.isExpanded) return 0; const total = node.data.items?.length ?? 0; diff --git a/src/lib/components/ui/checkbox/checkbox.svelte b/src/lib/components/ui/checkbox/checkbox.svelte index 1622e05..1978a19 100644 --- a/src/lib/components/ui/checkbox/checkbox.svelte +++ b/src/lib/components/ui/checkbox/checkbox.svelte @@ -1,8 +1,8 @@ diff --git a/src/lib/constants.ts b/src/lib/constants.ts index b3eb794..9f1344e 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -1,11 +1,10 @@ export const STORAGE_KEYS = { - URL: 'pdjsoneditor_url', - METHOD: 'pdjsoneditor_method', - HEADERS: 'pdjsoneditor_headers', - BODY: 'pdjsoneditor_body', - RAW_BODY_MODE: 'pdjsoneditor_raw_body_mode', - USE_EDITOR_CONTENT: 'pdjsoneditor_use_editor_content' + URL: 'pdjsoneditor_url', + METHOD: 'pdjsoneditor_method', + HEADERS: 'pdjsoneditor_headers', + BODY: 'pdjsoneditor_body', + RAW_BODY_MODE: 'pdjsoneditor_raw_body_mode', + USE_EDITOR_CONTENT: 'pdjsoneditor_use_editor_content' } as const; export type StorageKey = (typeof STORAGE_KEYS)[keyof typeof STORAGE_KEYS]; - diff --git a/src/lib/logger.ts b/src/lib/logger.ts index 7a18ed0..3a8abbc 100644 --- a/src/lib/logger.ts +++ b/src/lib/logger.ts @@ -5,24 +5,23 @@ import { dev } from '$app/environment'; type Level = 'silly' | 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal'; const levelMap: Record = { - silly: 0, - trace: 1, - debug: 2, - info: 3, - warn: 4, - error: 5, - fatal: 6 + silly: 0, + trace: 1, + debug: 2, + info: 3, + warn: 4, + error: 5, + fatal: 6 }; function resolveMinLevel(): number { - const raw = env.PUBLIC_LOG_LEVEL?.toLowerCase() as Level | undefined; - if (raw && raw in levelMap) return levelMap[raw]; - // Default: debug in dev, warn in prod - return dev ? levelMap.debug : levelMap.warn; + const raw = env.PUBLIC_LOG_LEVEL?.toLowerCase() as Level | undefined; + if (raw && raw in levelMap) return levelMap[raw]; + // Default: debug in dev, warn in prod + return dev ? levelMap.debug : levelMap.warn; } export const logger = new Logger({ - name: 'pdjsoneditor', - minLevel: resolveMinLevel() + name: 'pdjsoneditor', + minLevel: resolveMinLevel() }); - diff --git a/src/lib/services/http.ts b/src/lib/services/http.ts index 4784e88..3d7cd99 100644 --- a/src/lib/services/http.ts +++ b/src/lib/services/http.ts @@ -3,100 +3,99 @@ export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; export type HeaderKV = { key: string; value: string }; function normalizeAndBuildHeaders(pairs: HeaderKV[]): Record { - const headers: Record = {}; - for (const h of pairs) { - if (!h?.key || !h?.value) continue; - // Overwrite case-insensitively - const existing = Object.keys(headers).find((k) => k.toLowerCase() === h.key.toLowerCase()); - if (existing) delete headers[existing]; - headers[h.key] = h.value; - } - return headers; + const headers: Record = {}; + for (const h of pairs) { + if (!h?.key || !h?.value) continue; + // Overwrite case-insensitively + const existing = Object.keys(headers).find((k) => k.toLowerCase() === h.key.toLowerCase()); + if (existing) delete headers[existing]; + headers[h.key] = h.value; + } + return headers; } export interface RequestJsonOptions { - method: HttpMethod; - url: string; - headers: HeaderKV[]; - editorJson: string; - customBody: string; - sendAsRawText: boolean; - useEditorContent: boolean; - signal?: AbortSignal; + method: HttpMethod; + url: string; + headers: HeaderKV[]; + editorJson: string; + customBody: string; + sendAsRawText: boolean; + useEditorContent: boolean; + signal?: AbortSignal; } export interface RequestJsonResult { - ok: boolean; - status: number; - data?: unknown; - rawText?: string; - contentType?: string | null; + ok: boolean; + status: number; + data?: unknown; + rawText?: string; + contentType?: string | null; } export async function requestJson(opts: RequestJsonOptions): Promise { - const { - method, - url, - headers: headerPairs, - editorJson, - customBody, - sendAsRawText, - useEditorContent, - signal - } = opts; + const { + method, + url, + headers: headerPairs, + editorJson, + customBody, + sendAsRawText, + useEditorContent, + signal + } = opts; - const headers = normalizeAndBuildHeaders(headerPairs); - const init: RequestInit = { method, headers, signal }; + const headers = normalizeAndBuildHeaders(headerPairs); + const init: RequestInit = { method, headers, signal }; - if (method === 'POST' || method === 'PUT' || method === 'PATCH') { - const bodyContent = (useEditorContent ? editorJson : customBody).trim(); - if (bodyContent) { - if (sendAsRawText) { - (init as any).body = bodyContent; - } else { - // Ensure JSON body is valid and set proper content-type - const parsed = JSON.parse(bodyContent); - (init as any).body = JSON.stringify(parsed); - // Upsert Content-Type to application/json - const hasCT = Object.keys(headers).some((k) => k.toLowerCase() === 'content-type'); - if (hasCT) { - const ctKey = Object.keys(headers).find((k) => k.toLowerCase() === 'content-type')!; - headers[ctKey] = 'application/json'; - } else { - headers['Content-Type'] = 'application/json'; - } - } - } - } + if (method === 'POST' || method === 'PUT' || method === 'PATCH') { + const bodyContent = (useEditorContent ? editorJson : customBody).trim(); + if (bodyContent) { + if (sendAsRawText) { + (init as any).body = bodyContent; + } else { + // Ensure JSON body is valid and set proper content-type + const parsed = JSON.parse(bodyContent); + (init as any).body = JSON.stringify(parsed); + // Upsert Content-Type to application/json + const hasCT = Object.keys(headers).some((k) => k.toLowerCase() === 'content-type'); + if (hasCT) { + const ctKey = Object.keys(headers).find((k) => k.toLowerCase() === 'content-type')!; + headers[ctKey] = 'application/json'; + } else { + headers['Content-Type'] = 'application/json'; + } + } + } + } - const resp = await fetch(url, init); - const result: RequestJsonResult = { ok: resp.ok, status: resp.status }; + const resp = await fetch(url, init); + const result: RequestJsonResult = { ok: resp.ok, status: resp.status }; - const contentType = resp.headers.get('content-type'); - result.contentType = contentType; + const contentType = resp.headers.get('content-type'); + result.contentType = contentType; - // 204/205 or no content - if (resp.status === 204 || resp.status === 205) { - return result; - } + // 204/205 or no content + if (resp.status === 204 || resp.status === 205) { + return result; + } - const text = await resp.text(); - if (text && contentType?.toLowerCase().includes('application/json')) { - try { - result.data = JSON.parse(text); - } catch { - // Fallback to text if parsing fails - result.rawText = text; - } - } else { - // Try JSON parse anyway, else return raw text - try { - result.data = JSON.parse(text); - } catch { - result.rawText = text; - } - } + const text = await resp.text(); + if (text && contentType?.toLowerCase().includes('application/json')) { + try { + result.data = JSON.parse(text); + } catch { + // Fallback to text if parsing fails + result.rawText = text; + } + } else { + // Try JSON parse anyway, else return raw text + try { + result.data = JSON.parse(text); + } catch { + result.rawText = text; + } + } - return result; + return result; } - diff --git a/src/lib/stores/graphLoading.ts b/src/lib/stores/graphLoading.ts index 0b1b455..2fbcfed 100644 --- a/src/lib/stores/graphLoading.ts +++ b/src/lib/stores/graphLoading.ts @@ -3,14 +3,13 @@ import { writable } from 'svelte/store'; export type GraphPhase = 'idle' | 'build' | 'layout' | 'finalize'; export interface GraphLoadingState { - active: boolean; - phase: GraphPhase; - progress: number; // 0..1 + active: boolean; + phase: GraphPhase; + progress: number; // 0..1 } export const graphLoading = writable({ - active: false, - phase: 'idle', - progress: 0 + active: false, + phase: 'idle', + progress: 0 }); - diff --git a/src/lib/stores/requestSettings.ts b/src/lib/stores/requestSettings.ts index ab5b1d3..e6ad004 100644 --- a/src/lib/stores/requestSettings.ts +++ b/src/lib/stores/requestSettings.ts @@ -3,66 +3,77 @@ import type { HttpMethod, HeaderKV } from '$lib/services/http'; import { STORAGE_KEYS } from '$lib/constants'; export interface RequestSettingsState { - url: string; - method: HttpMethod; - headers: HeaderKV[]; - body: string; - sendAsRawText: boolean; - useEditorContent: boolean; + url: string; + method: HttpMethod; + headers: HeaderKV[]; + body: string; + sendAsRawText: boolean; + useEditorContent: boolean; } const getInitial = (): RequestSettingsState => { - if (typeof window === 'undefined') { - return { - url: 'https://jsonplaceholder.typicode.com/todos/1', - method: 'GET', - headers: [], - body: '', - sendAsRawText: false, - useEditorContent: false - }; - } + if (typeof window === 'undefined') { + return { + url: 'https://jsonplaceholder.typicode.com/todos/1', + method: 'GET', + headers: [], + body: '', + sendAsRawText: false, + useEditorContent: false + }; + } - let url = localStorage.getItem(STORAGE_KEYS.URL) || 'https://jsonplaceholder.typicode.com/todos/1'; - const rawMethod = localStorage.getItem(STORAGE_KEYS.METHOD) as HttpMethod | null; - const method: HttpMethod = rawMethod && ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'].includes(rawMethod) - ? rawMethod - : 'GET'; - let headers: HeaderKV[] = []; - const savedHeaders = localStorage.getItem(STORAGE_KEYS.HEADERS); - if (savedHeaders) { - try { headers = JSON.parse(savedHeaders); } catch {} - } - const body = localStorage.getItem(STORAGE_KEYS.BODY) || ''; - let sendAsRawText = false; - const savedRaw = localStorage.getItem(STORAGE_KEYS.RAW_BODY_MODE); - if (savedRaw) { - try { sendAsRawText = JSON.parse(savedRaw); } catch {} - } - let useEditorContent = false; - const savedUse = localStorage.getItem(STORAGE_KEYS.USE_EDITOR_CONTENT); - if (savedUse) { - try { useEditorContent = JSON.parse(savedUse); } catch {} - } + let url = + localStorage.getItem(STORAGE_KEYS.URL) || 'https://jsonplaceholder.typicode.com/todos/1'; + const rawMethod = localStorage.getItem(STORAGE_KEYS.METHOD) as HttpMethod | null; + const method: HttpMethod = + rawMethod && ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'].includes(rawMethod) ? rawMethod : 'GET'; + let headers: HeaderKV[] = []; + const savedHeaders = localStorage.getItem(STORAGE_KEYS.HEADERS); + if (savedHeaders) { + try { + headers = JSON.parse(savedHeaders); + } catch { + // Ignore parse errors for invalid headers data + } + } + const body = localStorage.getItem(STORAGE_KEYS.BODY) || ''; + let sendAsRawText = false; + const savedRaw = localStorage.getItem(STORAGE_KEYS.RAW_BODY_MODE); + if (savedRaw) { + try { + sendAsRawText = JSON.parse(savedRaw); + } catch { + // Ignore parse errors for invalid raw body mode + } + } + let useEditorContent = false; + const savedUse = localStorage.getItem(STORAGE_KEYS.USE_EDITOR_CONTENT); + if (savedUse) { + try { + useEditorContent = JSON.parse(savedUse); + } catch { + // Ignore parse errors for invalid use editor content flag + } + } - return { url, method, headers, body, sendAsRawText, useEditorContent }; + return { url, method, headers, body, sendAsRawText, useEditorContent }; }; export const requestSettings = writable(getInitial()); // Persist on change (browser only) if (typeof window !== 'undefined') { - requestSettings.subscribe((s) => { - try { - localStorage.setItem(STORAGE_KEYS.URL, s.url); - localStorage.setItem(STORAGE_KEYS.METHOD, s.method); - localStorage.setItem(STORAGE_KEYS.HEADERS, JSON.stringify(s.headers ?? [])); - localStorage.setItem(STORAGE_KEYS.BODY, s.body ?? ''); - localStorage.setItem(STORAGE_KEYS.RAW_BODY_MODE, JSON.stringify(!!s.sendAsRawText)); - localStorage.setItem(STORAGE_KEYS.USE_EDITOR_CONTENT, JSON.stringify(!!s.useEditorContent)); - } catch { - // ignore storage errors - } - }); + requestSettings.subscribe((s) => { + try { + localStorage.setItem(STORAGE_KEYS.URL, s.url); + localStorage.setItem(STORAGE_KEYS.METHOD, s.method); + localStorage.setItem(STORAGE_KEYS.HEADERS, JSON.stringify(s.headers ?? [])); + localStorage.setItem(STORAGE_KEYS.BODY, s.body ?? ''); + localStorage.setItem(STORAGE_KEYS.RAW_BODY_MODE, JSON.stringify(!!s.sendAsRawText)); + localStorage.setItem(STORAGE_KEYS.USE_EDITOR_CONTENT, JSON.stringify(!!s.useEditorContent)); + } catch { + // ignore storage errors + } + }); } - diff --git a/src/lib/types/json.ts b/src/lib/types/json.ts index 642aba6..369b253 100644 --- a/src/lib/types/json.ts +++ b/src/lib/types/json.ts @@ -20,4 +20,4 @@ export interface JsonStructure { _length?: number; _sample?: JsonStructure | string | null; [key: string]: JsonStructure | string | number | null | undefined; -} \ No newline at end of file +} diff --git a/src/lib/workers/graphLayout.worker.ts b/src/lib/workers/graphLayout.worker.ts index 6612f49..1290b11 100644 --- a/src/lib/workers/graphLayout.worker.ts +++ b/src/lib/workers/graphLayout.worker.ts @@ -5,183 +5,182 @@ import dagre from 'dagre'; type NodeData = { - label: string; - items?: any[]; - allItems?: any[]; - isExpanded?: boolean; - isArray?: boolean; - nodeId?: string; + label: string; + items?: any[]; + allItems?: any[]; + isExpanded?: boolean; + isArray?: boolean; + nodeId?: string; }; type FlowNode = { - id: string; - data: NodeData; - position: { x: number; y: number }; + id: string; + data: NodeData; + position: { x: number; y: number }; }; type Edge = { id: string; source: string; target: string }; type LayoutConfig = { - NODE_WIDTH: number; - MAX_DISPLAY_ITEMS: number; - METRICS: { - NODE_PADDING_Y: number; - NODE_BORDER_Y: number; - HEADER_HEIGHT: number; - ITEMS_TOP_MARGIN: number; - ITEM_ROW_HEIGHT: number; - MORE_BUTTON_HEIGHT: number; - }; - DAGRE: { - RANK_DIR: 'LR' | 'TB' | 'BT' | 'RL'; - NODE_SEP: number; - RANK_SEP: number; - EDGE_SEP: number; - RANKER: 'network-simplex' | 'tight-tree' | 'longest-path'; - ALIGN: 'UL' | 'UR' | 'DL' | 'DR' | undefined; - MARGIN_X: number; - MARGIN_Y: number; - }; - OVERLAP: { - MIN_SPACING: number; - X_TOLERANCE: number; - }; + NODE_WIDTH: number; + MAX_DISPLAY_ITEMS: number; + METRICS: { + NODE_PADDING_Y: number; + NODE_BORDER_Y: number; + HEADER_HEIGHT: number; + ITEMS_TOP_MARGIN: number; + ITEM_ROW_HEIGHT: number; + MORE_BUTTON_HEIGHT: number; + }; + DAGRE: { + RANK_DIR: 'LR' | 'TB' | 'BT' | 'RL'; + NODE_SEP: number; + RANK_SEP: number; + EDGE_SEP: number; + RANKER: 'network-simplex' | 'tight-tree' | 'longest-path'; + ALIGN: 'UL' | 'UR' | 'DL' | 'DR' | undefined; + MARGIN_X: number; + MARGIN_Y: number; + }; + OVERLAP: { + MIN_SPACING: number; + X_TOLERANCE: number; + }; }; type LayoutRequest = { - type: 'layout'; - nodes: FlowNode[]; - edges: Edge[]; - measuredHeights: Array<[string, number]>; // entries of Map - showAllItemsNodeIds: string[]; - config: LayoutConfig; + type: 'layout'; + nodes: FlowNode[]; + edges: Edge[]; + measuredHeights: Array<[string, number]>; // entries of Map + showAllItemsNodeIds: string[]; + config: LayoutConfig; }; type ProgressMsg = { type: 'progress'; phase: 'build' | 'layout'; progress: number }; type DoneMsg = { type: 'done'; nodes: FlowNode[] }; -type WorkerMsg = ProgressMsg | DoneMsg; const ctx: any = self; function estimateDisplayItemCount(node: FlowNode, cfg: LayoutConfig, showAll: boolean): number { - if (!node.data?.isExpanded) return 0; - const total = node.data.items?.length ?? 0; - if (showAll) return total; - return Math.min(total, cfg.MAX_DISPLAY_ITEMS); + if (!node.data?.isExpanded) return 0; + const total = node.data.items?.length ?? 0; + if (showAll) return total; + return Math.min(total, cfg.MAX_DISPLAY_ITEMS); } function hasMoreButton(node: FlowNode, cfg: LayoutConfig, showAll: boolean): boolean { - if (!node.data?.isExpanded) return false; - const total = node.data.items?.length ?? 0; - return total > cfg.MAX_DISPLAY_ITEMS && !showAll; + if (!node.data?.isExpanded) return false; + const total = node.data.items?.length ?? 0; + return total > cfg.MAX_DISPLAY_ITEMS && !showAll; } function estimateNodeHeight(node: FlowNode, cfg: LayoutConfig, showAll: boolean): number { - const M = cfg.METRICS; - let h = M.NODE_PADDING_Y + M.NODE_BORDER_Y + M.HEADER_HEIGHT; - if (node.data?.isExpanded) { - const count = estimateDisplayItemCount(node, cfg, showAll); - const extraBtn = hasMoreButton(node, cfg, showAll) ? M.MORE_BUTTON_HEIGHT : 0; - h += M.ITEMS_TOP_MARGIN + count * M.ITEM_ROW_HEIGHT + extraBtn; - } - return Math.max(h, 32); + const M = cfg.METRICS; + let h = M.NODE_PADDING_Y + M.NODE_BORDER_Y + M.HEADER_HEIGHT; + if (node.data?.isExpanded) { + const count = estimateDisplayItemCount(node, cfg, showAll); + const extraBtn = hasMoreButton(node, cfg, showAll) ? M.MORE_BUTTON_HEIGHT : 0; + h += M.ITEMS_TOP_MARGIN + count * M.ITEM_ROW_HEIGHT + extraBtn; + } + return Math.max(h, 32); } ctx.onmessage = (ev: MessageEvent) => { - const msg = ev.data; - if (msg.type !== 'layout') return; - - const { nodes, edges, measuredHeights, showAllItemsNodeIds, config } = msg; - - const measuredMap = new Map(measuredHeights); - const showAllSet = new Set(showAllItemsNodeIds); - - const graph = new dagre.graphlib.Graph(); - graph.setDefaultEdgeLabel(() => ({})); - - graph.setGraph({ - rankdir: config.DAGRE.RANK_DIR, - nodesep: config.DAGRE.NODE_SEP, - ranksep: config.DAGRE.RANK_SEP, - edgesep: config.DAGRE.EDGE_SEP, - ranker: config.DAGRE.RANKER, - align: config.DAGRE.ALIGN, - marginx: config.DAGRE.MARGIN_X, - marginy: config.DAGRE.MARGIN_Y - }); - - const total = nodes.length; - let processed = 0; - const report = (phase: 'build' | 'layout', p: number) => { - ctx.postMessage({ type: 'progress', phase, progress: p } as ProgressMsg); - }; - - // Build nodes - for (const n of nodes) { - const measured = measuredMap.get(n.id); - const showAll = showAllSet.has(n.id); - const height = measured ?? estimateNodeHeight(n, config, showAll); - graph.setNode(n.id, { width: config.NODE_WIDTH, height }); - processed++; - if (processed % 200 === 0) report('build', Math.min(0.9, processed / Math.max(1, total))); - } - // Build edges - for (const e of edges) { - graph.setEdge(e.source, e.target); - } - report('build', 1); - - // Run layout - report('layout', 0); - dagre.layout(graph); - report('layout', 1); - - // Map positions back - const result: FlowNode[] = nodes.map((node) => { - const g = graph.node(node.id) as dagre.Node; - const positioned: FlowNode = { - ...node, - position: { x: g.x, y: g.y } - }; - return positioned; - }); - - // Column overlap adjustment - const nodesByColumn = new Map>(); - for (const node of result) { - const g = graph.node(node.id) as dagre.Node; - if (!g) continue; - let foundKey: number | null = null; - for (const key of nodesByColumn.keys()) { - if (Math.abs(g.x - key) <= config.OVERLAP.X_TOLERANCE) { - foundKey = key; - break; - } - } - if (foundKey !== null) nodesByColumn.get(foundKey)!.push({ node, g }); - else nodesByColumn.set(g.x, [{ node, g }]); - } - - nodesByColumn.forEach((nodesInColumn) => { - if (nodesInColumn.length > 1) { - nodesInColumn.sort((a, b) => a.node.position.y - b.node.position.y); - for (let i = 1; i < nodesInColumn.length; i++) { - const prev = nodesInColumn[i - 1]; - const curr = nodesInColumn[i]; - const prevBottom = prev.node.position.y + prev.g.height / 2; - const currTop = curr.node.position.y - curr.g.height / 2; - const minSpacing = config.OVERLAP.MIN_SPACING; - if (currTop < prevBottom + minSpacing) { - const adj = prevBottom + minSpacing - currTop; - curr.node.position.y += adj; - for (let j = i + 1; j < nodesInColumn.length; j++) nodesInColumn[j].node.position.y += adj; - } - } - } - }); - - ctx.postMessage({ type: 'done', nodes: result } as DoneMsg); + const msg = ev.data; + if (msg.type !== 'layout') return; + + const { nodes, edges, measuredHeights, showAllItemsNodeIds, config } = msg; + + const measuredMap = new Map(measuredHeights); + const showAllSet = new Set(showAllItemsNodeIds); + + const graph = new dagre.graphlib.Graph(); + graph.setDefaultEdgeLabel(() => ({})); + + graph.setGraph({ + rankdir: config.DAGRE.RANK_DIR, + nodesep: config.DAGRE.NODE_SEP, + ranksep: config.DAGRE.RANK_SEP, + edgesep: config.DAGRE.EDGE_SEP, + ranker: config.DAGRE.RANKER, + align: config.DAGRE.ALIGN, + marginx: config.DAGRE.MARGIN_X, + marginy: config.DAGRE.MARGIN_Y + }); + + const total = nodes.length; + let processed = 0; + const report = (phase: 'build' | 'layout', p: number) => { + ctx.postMessage({ type: 'progress', phase, progress: p } as ProgressMsg); + }; + + // Build nodes + for (const n of nodes) { + const measured = measuredMap.get(n.id); + const showAll = showAllSet.has(n.id); + const height = measured ?? estimateNodeHeight(n, config, showAll); + graph.setNode(n.id, { width: config.NODE_WIDTH, height }); + processed++; + if (processed % 200 === 0) report('build', Math.min(0.9, processed / Math.max(1, total))); + } + // Build edges + for (const e of edges) { + graph.setEdge(e.source, e.target); + } + report('build', 1); + + // Run layout + report('layout', 0); + dagre.layout(graph); + report('layout', 1); + + // Map positions back + const result: FlowNode[] = nodes.map((node) => { + const g = graph.node(node.id) as dagre.Node; + const positioned: FlowNode = { + ...node, + position: { x: g.x, y: g.y } + }; + return positioned; + }); + + // Column overlap adjustment + const nodesByColumn = new Map>(); + for (const node of result) { + const g = graph.node(node.id) as dagre.Node; + if (!g) continue; + let foundKey: number | null = null; + for (const key of nodesByColumn.keys()) { + if (Math.abs(g.x - key) <= config.OVERLAP.X_TOLERANCE) { + foundKey = key; + break; + } + } + if (foundKey !== null) nodesByColumn.get(foundKey)!.push({ node, g }); + else nodesByColumn.set(g.x, [{ node, g }]); + } + + nodesByColumn.forEach((nodesInColumn) => { + if (nodesInColumn.length > 1) { + nodesInColumn.sort((a, b) => a.node.position.y - b.node.position.y); + for (let i = 1; i < nodesInColumn.length; i++) { + const prev = nodesInColumn[i - 1]; + const curr = nodesInColumn[i]; + const prevBottom = prev.node.position.y + prev.g.height / 2; + const currTop = curr.node.position.y - curr.g.height / 2; + const minSpacing = config.OVERLAP.MIN_SPACING; + if (currTop < prevBottom + minSpacing) { + const adj = prevBottom + minSpacing - currTop; + curr.node.position.y += adj; + for (let j = i + 1; j < nodesInColumn.length; j++) + nodesInColumn[j].node.position.y += adj; + } + } + } + }); + + ctx.postMessage({ type: 'done', nodes: result } as DoneMsg); }; export {}; - diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 768dba0..7600406 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -2,7 +2,7 @@ import { onMount } from 'svelte'; import JsonEditor from '$lib/components/JsonEditor.svelte'; import JsonGraph from '$lib/components/JsonGraph.svelte'; - import { Button } from '$lib/components/ui/button'; + import { Button } from '$lib/components/ui/button'; import { Input } from '$lib/components/ui/input'; import * as DropdownMenu from '$lib/components/ui/dropdown-menu'; import * as Dialog from '$lib/components/ui/dialog'; @@ -98,12 +98,12 @@ let editorRef: JsonEditor; let parseTimeout: ReturnType; - // LocalStorage keys are centralized in $lib/constants + // LocalStorage keys are centralized in $lib/constants // Initialize with empty values (will be populated from localStorage in onMount) let urlInput = $state('https://jsonplaceholder.typicode.com/todos/1'); let isLoading = $state(false); - let httpMethod = $state('GET'); + let httpMethod = $state('GET'); let isDialogOpen = $state(false); let customHeaders = $state>([]); let tempHeaders = $state>([]); @@ -116,10 +116,10 @@ let httpStatusCode = $state(null); let responseTime = $state(null); - const httpMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'] as const; + const httpMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'] as const; - // Track in-flight request for cancellation - let abortController: AbortController | null = null; + // Track in-flight request for cancellation + let abortController: AbortController | null = null; function openSettingsDialog() { // Copy current headers to temp, ensure it's a proper array @@ -162,7 +162,7 @@ function clearAllSettings() { if (confirm($LL.editor.clearAllConfirm())) { // Clear all localStorage keys - Object.values(STORAGE_KEYS).forEach(key => { + Object.values(STORAGE_KEYS).forEach((key) => { localStorage.removeItem(key); }); @@ -255,70 +255,70 @@ } } - async function fetchJsonFromUrl() { - if (!urlInput.trim()) { - error = $LL.editor.urlRequired(); - return; - } - - isLoading = true; - // Only clear error if it's a fetch-related error - if (error && (error.includes('fetch') || error.includes('HTTP'))) { - error = ''; - } - httpStatusCode = null; - responseTime = null; - - try { - // Cancel previous request if any - if (abortController) { - abortController.abort(); - } - abortController = new AbortController(); - - const startTime = performance.now(); - const res = await requestJson({ - method: httpMethod as HttpMethod, - url: urlInput, - headers: customHeaders, - editorJson: jsonValue, - customBody, - sendAsRawText, - useEditorContent, - signal: abortController.signal - }); - const endTime = performance.now(); - responseTime = Math.round(endTime - startTime); - httpStatusCode = res.status; - - if (res.data !== undefined) { - jsonValue = JSON.stringify(res.data, null, 2); - } else if (res.rawText !== undefined) { - // Non-JSON 응답은 텍스트로 보여줌 - jsonValue = JSON.stringify({ response: res.rawText }, null, 2); - } - - if (res.ok && error && (error.includes('fetch') || error.includes('HTTP'))) { - error = ''; - } - } catch (e) { - if ((e as any)?.name === 'AbortError') { - // silently ignore aborted request - return; - } - if (e instanceof Error) { - if (e.message.includes('Failed to fetch')) { - error = $LL.editor.fetchError(); - } else { - error = e.message; - } - } else { - error = $LL.editor.fetchError(); - } - } finally { - isLoading = false; - } - } + async function fetchJsonFromUrl() { + if (!urlInput.trim()) { + error = $LL.editor.urlRequired(); + return; + } + + isLoading = true; + // Only clear error if it's a fetch-related error + if (error && (error.includes('fetch') || error.includes('HTTP'))) { + error = ''; + } + httpStatusCode = null; + responseTime = null; + + try { + // Cancel previous request if any + if (abortController) { + abortController.abort(); + } + abortController = new AbortController(); + + const startTime = performance.now(); + const res = await requestJson({ + method: httpMethod as HttpMethod, + url: urlInput, + headers: customHeaders, + editorJson: jsonValue, + customBody, + sendAsRawText, + useEditorContent, + signal: abortController.signal + }); + const endTime = performance.now(); + responseTime = Math.round(endTime - startTime); + httpStatusCode = res.status; + + if (res.data !== undefined) { + jsonValue = JSON.stringify(res.data, null, 2); + } else if (res.rawText !== undefined) { + // Non-JSON 응답은 텍스트로 보여줌 + jsonValue = JSON.stringify({ response: res.rawText }, null, 2); + } + + if (res.ok && error && (error.includes('fetch') || error.includes('HTTP'))) { + error = ''; + } + } catch (e) { + if ((e as any)?.name === 'AbortError') { + // silently ignore aborted request + return; + } + if (e instanceof Error) { + if (e.message.includes('Failed to fetch')) { + error = $LL.editor.fetchError(); + } else { + error = e.message; + } + } else { + error = $LL.editor.fetchError(); + } + } finally { + isLoading = false; + } + } function handleUrlKeydown(e: KeyboardEvent) { if (e.key === 'Enter') { @@ -625,7 +625,7 @@

{$LL.editor.headers()}

-
= 4 ? "max-h-48 overflow-y-auto space-y-2" : "space-y-2"}> +
= 4 ? 'max-h-48 overflow-y-auto space-y-2' : 'space-y-2'}> {#each tempHeaders as header, index}
+ - - - - PDJSONEditor API Test Dashboard - - - -
-

🧪 PDJSONEditor API Test Dashboard

- -
- - Checking server status... -
- -
- -
-

GET Requests

- - - - - - -
- - -
-

POST Requests

- - - -
- - -
-

PUT/PATCH Requests

- - - -
- - -
-

DELETE Requests

- - -
- - -
-

Error Handling

- - - - -
- - -
-

Headers & Custom

- - -
-
- -
-

Test Results

-
- Click any test button to see the results here... -
-
-
- - - - \ No newline at end of file + + + + PDJSONEditor API Test Dashboard + + + +
+

🧪 PDJSONEditor API Test Dashboard

+ +
+ + Checking server status... +
+ +
+ +
+

GET Requests

+ + + + + + +
+ + +
+

POST Requests

+ + + +
+ + +
+

PUT/PATCH Requests

+ + + +
+ + +
+

DELETE Requests

+ + +
+ + +
+

Error Handling

+ + + + +
+ + +
+

Headers & Custom

+ + +
+
+ +
+

Test Results

+
+ Click any test button to see the results here... +
+
+
+ + + + diff --git a/test-server.cjs b/test-server.cjs index a23262d..ecf1c1c 100644 --- a/test-server.cjs +++ b/test-server.cjs @@ -13,31 +13,31 @@ const sampleData = { users: [ { id: 1, - name: "John Doe", - email: "john@example.com", + name: 'John Doe', + email: 'john@example.com', age: 30, active: true, profile: { - bio: "Software Developer", - location: "San Francisco", - interests: ["coding", "reading", "hiking"] + bio: 'Software Developer', + location: 'San Francisco', + interests: ['coding', 'reading', 'hiking'] } }, { id: 2, - name: "Jane Smith", - email: "jane@example.com", + name: 'Jane Smith', + email: 'jane@example.com', age: 28, active: false, profile: { - bio: "Product Designer", - location: "New York", - interests: ["design", "photography", "travel"] + bio: 'Product Designer', + location: 'New York', + interests: ['design', 'photography', 'travel'] } } ], metadata: { - version: "1.0.0", + version: '1.0.0', timestamp: new Date().toISOString(), totalUsers: 2 } @@ -48,27 +48,27 @@ const productsData = { products: [ { id: 101, - name: "Laptop Pro", + name: 'Laptop Pro', price: 1299.99, stock: 15, - categories: ["Electronics", "Computers"], + categories: ['Electronics', 'Computers'], specifications: { - cpu: "Intel Core i7", - ram: "16GB", - storage: "512GB SSD", - display: "15.6 inch" + cpu: 'Intel Core i7', + ram: '16GB', + storage: '512GB SSD', + display: '15.6 inch' } }, { id: 102, - name: "Wireless Mouse", + name: 'Wireless Mouse', price: 29.99, stock: 50, - categories: ["Electronics", "Accessories"], + categories: ['Electronics', 'Accessories'], specifications: { - connectivity: "Bluetooth 5.0", - battery: "AA x 2", - dpi: "1600" + connectivity: 'Bluetooth 5.0', + battery: 'AA x 2', + dpi: '1600' } } ] @@ -77,34 +77,34 @@ const productsData = { // Complex nested structure for testing const complexData = { company: { - name: "Tech Corp", + name: 'Tech Corp', founded: 2010, employees: 150, departments: [ { - name: "Engineering", + name: 'Engineering', headCount: 80, teams: [ { - name: "Frontend", + name: 'Frontend', members: 25, - technologies: ["React", "Vue", "Svelte"] + technologies: ['React', 'Vue', 'Svelte'] }, { - name: "Backend", + name: 'Backend', members: 30, - technologies: ["Node.js", "Python", "Go"] + technologies: ['Node.js', 'Python', 'Go'] } ] }, { - name: "Marketing", + name: 'Marketing', headCount: 30, campaigns: [ { - name: "Summer Sale", + name: 'Summer Sale', budget: 50000, - channels: ["Social Media", "Email", "PPC"] + channels: ['Social Media', 'Email', 'PPC'] } ] } @@ -114,10 +114,10 @@ const complexData = { expenses: 12000000, profit: 3000000, quarters: [ - { q: "Q1", revenue: 3500000 }, - { q: "Q2", revenue: 3800000 }, - { q: "Q3", revenue: 3700000 }, - { q: "Q4", revenue: 4000000 } + { q: 'Q1', revenue: 3500000 }, + { q: 'Q2', revenue: 3800000 }, + { q: 'Q3', revenue: 3700000 }, + { q: 'Q4', revenue: 4000000 } ] } } @@ -125,10 +125,10 @@ const complexData = { // Array of objects for testing const arrayData = [ - { id: 1, type: "info", message: "System initialized", timestamp: Date.now() }, - { id: 2, type: "warning", message: "High memory usage", timestamp: Date.now() - 1000 }, - { id: 3, type: "error", message: "Connection timeout", timestamp: Date.now() - 2000 }, - { id: 4, type: "info", message: "Backup completed", timestamp: Date.now() - 3000 } + { id: 1, type: 'info', message: 'System initialized', timestamp: Date.now() }, + { id: 2, type: 'warning', message: 'High memory usage', timestamp: Date.now() - 1000 }, + { id: 3, type: 'error', message: 'Connection timeout', timestamp: Date.now() - 2000 }, + { id: 4, type: 'info', message: 'Backup completed', timestamp: Date.now() - 3000 } ]; // Store for POST/PUT/PATCH testing @@ -176,11 +176,11 @@ app.get('/api/search', (req, res) => { app.get('/api/users/:id', (req, res) => { const userId = parseInt(req.params.id); console.log(`[GET /api/users/${userId}] Request received`); - const user = dynamicData.users.find(u => u.id === userId); + const user = dynamicData.users.find((u) => u.id === userId); if (user) { res.json(user); } else { - res.status(404).json({ error: "User not found", userId }); + res.status(404).json({ error: 'User not found', userId }); } }); @@ -194,7 +194,7 @@ app.post('/api/users', (req, res) => { }; dynamicData.users.push(newUser); res.status(201).json({ - message: "User created successfully", + message: 'User created successfully', user: newUser, totalUsers: dynamicData.users.length }); @@ -214,8 +214,8 @@ app.post('/api/echo', (req, res) => { app.put('/api/users/:id', (req, res) => { const userId = parseInt(req.params.id); console.log(`[PUT /api/users/${userId}] Body:`, req.body); - const userIndex = dynamicData.users.findIndex(u => u.id === userId); - + const userIndex = dynamicData.users.findIndex((u) => u.id === userId); + if (userIndex !== -1) { dynamicData.users[userIndex] = { ...dynamicData.users[userIndex], @@ -224,11 +224,11 @@ app.put('/api/users/:id', (req, res) => { updatedAt: new Date().toISOString() }; res.json({ - message: "User updated successfully", + message: 'User updated successfully', user: dynamicData.users[userIndex] }); } else { - res.status(404).json({ error: "User not found", userId }); + res.status(404).json({ error: 'User not found', userId }); } }); @@ -236,22 +236,22 @@ app.put('/api/users/:id', (req, res) => { app.patch('/api/users/:id', (req, res) => { const userId = parseInt(req.params.id); console.log(`[PATCH /api/users/${userId}] Body:`, req.body); - const userIndex = dynamicData.users.findIndex(u => u.id === userId); - + const userIndex = dynamicData.users.findIndex((u) => u.id === userId); + if (userIndex !== -1) { // PATCH only updates provided fields - Object.keys(req.body).forEach(key => { + Object.keys(req.body).forEach((key) => { dynamicData.users[userIndex][key] = req.body[key]; }); dynamicData.users[userIndex].patchedAt = new Date().toISOString(); - + res.json({ - message: "User patched successfully", + message: 'User patched successfully', user: dynamicData.users[userIndex], patchedFields: Object.keys(req.body) }); } else { - res.status(404).json({ error: "User not found", userId }); + res.status(404).json({ error: 'User not found', userId }); } }); @@ -259,17 +259,17 @@ app.patch('/api/users/:id', (req, res) => { app.delete('/api/users/:id', (req, res) => { const userId = parseInt(req.params.id); console.log(`[DELETE /api/users/${userId}] Request received`); - const userIndex = dynamicData.users.findIndex(u => u.id === userId); - + const userIndex = dynamicData.users.findIndex((u) => u.id === userId); + if (userIndex !== -1) { const deletedUser = dynamicData.users.splice(userIndex, 1)[0]; res.json({ - message: "User deleted successfully", + message: 'User deleted successfully', deletedUser, remainingUsers: dynamicData.users.length }); } else { - res.status(404).json({ error: "User not found", userId }); + res.status(404).json({ error: 'User not found', userId }); } }); @@ -287,11 +287,11 @@ app.all('/api/headers', (req, res) => { // Error simulation endpoints app.get('/api/error/400', (req, res) => { - res.status(400).json({ error: "Bad Request", message: "Invalid parameters" }); + res.status(400).json({ error: 'Bad Request', message: 'Invalid parameters' }); }); app.get('/api/error/500', (req, res) => { - res.status(500).json({ error: "Internal Server Error", message: "Something went wrong" }); + res.status(500).json({ error: 'Internal Server Error', message: 'Something went wrong' }); }); // Slow response simulation @@ -334,7 +334,7 @@ app.post('/api/reset', (req, res) => { console.log('[POST /api/reset] Resetting dynamic data'); dynamicData = { ...sampleData }; res.json({ - message: "Data reset successfully", + message: 'Data reset successfully', timestamp: new Date().toISOString() }); }); @@ -342,26 +342,26 @@ app.post('/api/reset', (req, res) => { // Root endpoint app.get('/', (req, res) => { res.json({ - message: "PDJSONEditor Test Server", - version: "1.0.0", + message: 'PDJSONEditor Test Server', + version: '1.0.0', endpoints: [ - "GET /api/users - Get all users", - "GET /api/users/:id - Get specific user", - "POST /api/users - Create new user", - "PUT /api/users/:id - Update user", - "PATCH /api/users/:id - Patch user", - "DELETE /api/users/:id - Delete user", - "GET /api/products - Get products data", - "GET /api/complex - Get complex nested data", - "GET /api/array - Get array data", - "GET /api/search?q=query&limit=10 - Search with query params", - "POST /api/echo - Echo back the request", - "ALL /api/headers - Test headers", - "GET /api/error/400 - Simulate 400 error", - "GET /api/error/500 - Simulate 500 error", - "GET /api/slow?delay=3000 - Slow response", - "GET /api/large?size=100 - Large dataset", - "POST /api/reset - Reset dynamic data" + 'GET /api/users - Get all users', + 'GET /api/users/:id - Get specific user', + 'POST /api/users - Create new user', + 'PUT /api/users/:id - Update user', + 'PATCH /api/users/:id - Patch user', + 'DELETE /api/users/:id - Delete user', + 'GET /api/products - Get products data', + 'GET /api/complex - Get complex nested data', + 'GET /api/array - Get array data', + 'GET /api/search?q=query&limit=10 - Search with query params', + 'POST /api/echo - Echo back the request', + 'ALL /api/headers - Test headers', + 'GET /api/error/400 - Simulate 400 error', + 'GET /api/error/500 - Simulate 500 error', + 'GET /api/slow?delay=3000 - Slow response', + 'GET /api/large?size=100 - Large dataset', + 'POST /api/reset - Reset dynamic data' ] }); }); @@ -377,4 +377,4 @@ app.listen(PORT, () => { console.log(` - POST: http://localhost:${PORT}/api/echo`); console.log(` - GET: http://localhost:${PORT}/api/search?q=test&limit=5`); console.log('\nPress Ctrl+C to stop the server\n'); -}); \ No newline at end of file +}); diff --git a/tests/e2e/basic.spec.ts b/tests/e2e/basic.spec.ts index 08ba697..2fd1f37 100644 --- a/tests/e2e/basic.spec.ts +++ b/tests/e2e/basic.spec.ts @@ -12,10 +12,10 @@ test.describe('PDJSONEditor Basic Functionality', () => { test('should load the editor with default JSON', async ({ page }) => { // Check that the editor is visible await expect(page.locator('.cm-editor')).toBeVisible(); - + // Check that the graph view is visible await expect(page.locator('.svelte-flow')).toBeVisible(); - + // Verify initial JSON is present const content = await editorPage.getEditorContent(); expect(content).toContain('donut'); @@ -23,14 +23,14 @@ test.describe('PDJSONEditor Basic Functionality', () => { expect(content).toContain('batters'); }); - test('should format JSON when Format button is clicked', async ({ page }) => { + test('should format JSON when Format button is clicked', async () => { // Set minified JSON const minifiedJson = '{"name":"Test","age":25,"active":true}'; await editorPage.setEditorContent(minifiedJson); - + // Click format button await editorPage.formatJSON(); - + // Check that JSON is formatted (should have line breaks) const formatted = await editorPage.getEditorContent(); expect(formatted.split('\n').length).toBeGreaterThan(1); @@ -38,7 +38,7 @@ test.describe('PDJSONEditor Basic Functionality', () => { expect(() => JSON.parse(formatted)).not.toThrow(); }); - test('should minify JSON when Minify button is clicked', async ({ page }) => { + test('should minify JSON when Minify button is clicked', async () => { // Start with formatted JSON const formattedJson = `{ "name": "Test", @@ -46,10 +46,10 @@ test.describe('PDJSONEditor Basic Functionality', () => { "active": true }`; await editorPage.setEditorContent(formattedJson); - + // Click minify button await editorPage.minifyJSON(); - + // Check that JSON is minified (should have fewer lines) const minified = await editorPage.getEditorContent(); // Minified JSON should have significantly fewer lines than formatted @@ -62,18 +62,18 @@ test.describe('PDJSONEditor Basic Functionality', () => { test('should update graph when JSON is modified', async ({ page }) => { // Wait for initial graph to render await page.waitForSelector('.svelte-flow__node', { timeout: 5000 }); - + // Get initial node count const initialNodeCount = await editorPage.getNodeCount(); expect(initialNodeCount).toBeGreaterThan(0); - + // Update JSON with more complex structure const newJson = `{"users":[{"id":1,"name":"User 1"},{"id":2,"name":"User 2"}],"settings":{"theme":"dark","language":"en"}}`; await editorPage.setEditorContent(newJson); - + // Wait for graph to update await page.waitForTimeout(2000); - + // Check that node count has changed const newNodeCount = await editorPage.getNodeCount(); expect(newNodeCount).toBeGreaterThan(0); @@ -84,10 +84,10 @@ test.describe('PDJSONEditor Basic Functionality', () => { // Enter invalid JSON const invalidJson = '{ "name": "Test", "age": }'; await editorPage.setEditorContent(invalidJson); - + // Wait for error to appear await page.waitForTimeout(500); - + // Check for error message const errorElement = page.locator('text=/Invalid JSON|Unexpected token/i'); await expect(errorElement).toBeVisible(); @@ -97,10 +97,10 @@ test.describe('PDJSONEditor Basic Functionality', () => { // Check that both panels are visible const editorPanel = page.locator('.cm-editor'); const graphPanel = page.locator('.svelte-flow'); - + await expect(editorPanel).toBeVisible(); await expect(graphPanel).toBeVisible(); - + // Test resizable functionality (if implemented) const resizeHandle = page.locator('[data-panel-resize-handle]'); if (await resizeHandle.isVisible()) { @@ -123,48 +123,48 @@ test.describe('PDJSONEditor Basic Functionality', () => { } } })); - + const largeJson = JSON.stringify({ data: largeArray }, null, 2); await editorPage.setEditorContent(largeJson); - + // Wait for graph to render await page.waitForTimeout(2000); - + // Verify graph has rendered nodes const nodeCount = await editorPage.getNodeCount(); expect(nodeCount).toBeGreaterThan(0); - + // Verify no errors await editorPage.waitForNoErrors(); }); test('should preserve JSON structure after format/minify cycle', async ({ page }) => { const originalJson = { - name: "Test User", + name: 'Test User', age: 30, active: true, - tags: ["developer", "tester"], + tags: ['developer', 'tester'], address: { - city: "San Francisco", - country: "USA" + city: 'San Francisco', + country: 'USA' } }; - + // Set original JSON await editorPage.setEditorContent(JSON.stringify(originalJson)); - + // Format await editorPage.formatJSON(); await page.waitForTimeout(500); - + // Minify await editorPage.minifyJSON(); await page.waitForTimeout(500); - + // Parse and compare const finalContent = await editorPage.getEditorContent(); const finalJson = JSON.parse(finalContent); - + expect(finalJson).toEqual(originalJson); }); -}); \ No newline at end of file +}); diff --git a/tests/e2e/helpers/page-helpers.ts b/tests/e2e/helpers/page-helpers.ts index 8d062ef..fcbf088 100644 --- a/tests/e2e/helpers/page-helpers.ts +++ b/tests/e2e/helpers/page-helpers.ts @@ -20,7 +20,7 @@ export class PDJSONEditorPage { // CodeMirror uses contenteditable, so we need to get the text content // First wait for content to be rendered await this.page.waitForTimeout(100); - + // Try multiple methods to get the content // Method 1: Get all text from cm-content (most reliable for large content) const cmContent = await this.page.locator('.cm-content').textContent(); @@ -28,38 +28,38 @@ export class PDJSONEditorPage { // Clean up the content - remove any extra whitespace return cmContent.trim(); } - + // Method 2: Get all cm-line elements and concatenate their text const lines = await this.page.locator('.cm-line').allTextContents(); - + // If no lines found or empty, return empty string if (lines.length === 0 || (lines.length === 1 && lines[0] === '')) { return ''; } - + // Join lines and clean up any trailing whitespace or empty lines let content = lines.join('\n'); - + // Remove trailing whitespace and empty lines content = content.replace(/\s+$/g, ''); - + return content; } async setEditorContent(json: string) { // Click in the editor to focus it await this.page.locator('.cm-content').click(); - + // Select all text using keyboard shortcut await this.page.keyboard.press('Meta+A'); - + // Wait a bit for selection to complete await this.page.waitForTimeout(50); - + // Use insertText to replace the selected text // This is more reliable than Delete + type await this.page.keyboard.insertText(json); - + // Wait for CodeMirror to update await this.page.waitForTimeout(200); } @@ -78,7 +78,10 @@ export class PDJSONEditorPage { async selectHTTPMethod(method: string) { // Click on the method dropdown button (it shows current method) // The button contains the method text but may have additional elements - const dropdownButton = this.page.locator('button').filter({ hasText: /GET|POST|PUT|PATCH|DELETE/ }).first(); + const dropdownButton = this.page + .locator('button') + .filter({ hasText: /GET|POST|PUT|PATCH|DELETE/ }) + .first(); await dropdownButton.click(); // Select the desired method from dropdown await this.page.locator(`[role="menuitem"]:has-text("${method}")`).click(); @@ -109,16 +112,19 @@ export class PDJSONEditorPage { // First try by title attribute (works for English) let settingsButton = this.page.locator('button[title*="Request"]').first(); const count = await settingsButton.count(); - + if (count === 0) { // If not found by title, find the small button with icon after URL input // It's the button right before the "Go" button - const goButton = this.page.getByRole('button', { name: 'Go' }); - settingsButton = this.page.locator('button.h-7.px-2').filter({ - has: this.page.locator('svg') - }).first(); + // const goButton = this.page.getByRole('button', { name: 'Go' }); + settingsButton = this.page + .locator('button.h-7.px-2') + .filter({ + has: this.page.locator('svg') + }) + .first(); } - + await settingsButton.click(); // Wait for dialog to open await this.page.waitForSelector('[role="dialog"]', { timeout: 5000 }); @@ -128,19 +134,19 @@ export class PDJSONEditorPage { if (openDialog) { await this.openRequestSettings(); } - + // Wait for dialog inputs to be ready await this.page.waitForTimeout(200); - + // Find the last header row and fill it const keyInputs = this.page.locator('[role="dialog"] input[placeholder="Key"]'); const valueInputs = this.page.locator('[role="dialog"] input[placeholder="Value"]'); - + const keyCount = await keyInputs.count(); if (keyCount > 0) { await keyInputs.nth(keyCount - 1).fill(key); await valueInputs.nth(keyCount - 1).fill(value); - + // Add another row for next header const addButton = this.page.locator('[role="dialog"] button:has-text("Add Header")').first(); if (await addButton.isVisible()) { @@ -154,10 +160,10 @@ export class PDJSONEditorPage { if (openDialog) { await this.openRequestSettings(); } - + // Wait for dialog to be ready await this.page.waitForTimeout(200); - + // Fill in the custom body directly in the textarea const textarea = this.page.locator('[role="dialog"] textarea').first(); await textarea.fill(body); @@ -195,15 +201,15 @@ export class PDJSONEditorPage { } // Validation helpers - async validateJSONInEditor(expectedJson: any) { + async validateJSONInEditor(expectedJson: unknown) { const content = await this.getEditorContent(); let actualJson; try { actualJson = JSON.parse(content); - } catch (e) { + } catch { throw new Error(`Invalid JSON in editor: ${content}`); } - + expect(actualJson).toEqual(expectedJson); } @@ -220,13 +226,13 @@ export class PDJSONEditorPage { // Utility functions async takeScreenshot(name: string) { - await this.page.screenshot({ + await this.page.screenshot({ path: `test-results/screenshots/${name}.png`, - fullPage: true + fullPage: true }); } async waitForNetworkIdle() { await this.page.waitForLoadState('networkidle'); } -} \ No newline at end of file +} diff --git a/tests/e2e/http-requests.spec.ts b/tests/e2e/http-requests.spec.ts index 9465c01..8d2bcc7 100644 --- a/tests/e2e/http-requests.spec.ts +++ b/tests/e2e/http-requests.spec.ts @@ -7,13 +7,13 @@ test.describe('HTTP Request Functionality', () => { test.beforeEach(async ({ page }) => { editorPage = new PDJSONEditorPage(page); - + // Reset test server data await fetch(`${TEST_API_URL}/api/reset`, { method: 'POST' }); await page.waitForTimeout(500); - + await editorPage.goto(); - + // Clear any existing JSON to start fresh await editorPage.setEditorContent('{}'); }); @@ -22,15 +22,15 @@ test.describe('HTTP Request Functionality', () => { test('should fetch JSON data with GET request', async ({ page }) => { // Make GET request to fetch users await editorPage.makeHTTPRequest('GET', `${TEST_API_URL}/api/users`); - + // Wait for response await page.waitForTimeout(2000); - + // Verify JSON is loaded in editor const content = await editorPage.getEditorContent(); expect(content).toContain('users'); expect(content).toContain('John Doe'); - + // Verify graph is updated const nodeCount = await editorPage.getNodeCount(); expect(nodeCount).toBeGreaterThan(0); @@ -39,10 +39,10 @@ test.describe('HTTP Request Functionality', () => { test('should handle GET request with query parameters', async ({ page }) => { // Make GET request with query params await editorPage.makeHTTPRequest('GET', `${TEST_API_URL}/api/search?q=test&limit=5`); - + // Wait for response await page.waitForTimeout(2000); - + // Verify response contains query information const content = await editorPage.getEditorContent(); expect(content).toContain('query'); @@ -53,10 +53,10 @@ test.describe('HTTP Request Functionality', () => { test('should fetch array data', async ({ page }) => { // Make GET request for array data await editorPage.makeHTTPRequest('GET', `${TEST_API_URL}/api/array`); - + // Wait for response await page.waitForTimeout(2000); - + // Verify array is loaded const content = await editorPage.getEditorContent(); const json = JSON.parse(content); @@ -67,16 +67,16 @@ test.describe('HTTP Request Functionality', () => { test('should handle complex nested JSON', async ({ page }) => { // Make GET request for complex data await editorPage.makeHTTPRequest('GET', `${TEST_API_URL}/api/complex`); - + // Wait for response await page.waitForTimeout(2000); - + // Verify complex structure is loaded const content = await editorPage.getEditorContent(); expect(content).toContain('company'); expect(content).toContain('departments'); expect(content).toContain('financials'); - + // Verify graph shows nested structure const nodeCount = await editorPage.getNodeCount(); expect(nodeCount).toBeGreaterThan(5); @@ -94,13 +94,13 @@ test.describe('HTTP Request Functionality', () => { } }; await editorPage.setEditorContent(JSON.stringify(testData, null, 2)); - + // Make POST request to echo endpoint await editorPage.makeHTTPRequest('POST', `${TEST_API_URL}/api/echo`); - + // Wait for response await page.waitForTimeout(2000); - + // Verify echo response contains our data const content = await editorPage.getEditorContent(); const response = JSON.parse(content); @@ -115,13 +115,13 @@ test.describe('HTTP Request Functionality', () => { age: 25 }; await editorPage.setEditorContent(JSON.stringify(newUser, null, 2)); - + // Make POST request to create user await editorPage.makeHTTPRequest('POST', `${TEST_API_URL}/api/users`); - + // Wait for response await page.waitForTimeout(2000); - + // Verify success response const content = await editorPage.getEditorContent(); expect(content).toContain('User created successfully'); @@ -138,13 +138,13 @@ test.describe('HTTP Request Functionality', () => { age: 30 }; await editorPage.setEditorContent(JSON.stringify(updateData, null, 2)); - + // Make PUT request await editorPage.makeHTTPRequest('PUT', `${TEST_API_URL}/api/users/1`); - + // Wait for response await page.waitForTimeout(2000); - + // Verify update response const content = await editorPage.getEditorContent(); expect(content).toContain('User updated successfully'); @@ -157,13 +157,13 @@ test.describe('HTTP Request Functionality', () => { age: 35 }; await editorPage.setEditorContent(JSON.stringify(patchData, null, 2)); - + // Make PATCH request await editorPage.makeHTTPRequest('PATCH', `${TEST_API_URL}/api/users/1`); - + // Wait for response await page.waitForTimeout(2000); - + // Verify patch response const content = await editorPage.getEditorContent(); expect(content).toContain('User patched successfully'); @@ -175,25 +175,25 @@ test.describe('HTTP Request Functionality', () => { test('should delete resource with DELETE', async ({ page }) => { // First create a user to delete const newUser = { - name: "Test User To Delete", - email: "delete@test.com", + name: 'Test User To Delete', + email: 'delete@test.com', age: 25 }; await editorPage.setEditorContent(JSON.stringify(newUser, null, 2)); await editorPage.makeHTTPRequest('POST', `${TEST_API_URL}/api/users`); await page.waitForTimeout(1000); - + // Get the created user ID from response const createResponse = await editorPage.getEditorContent(); const createdUser = JSON.parse(createResponse); const userId = createdUser.user?.id || 3; - + // Now delete the created user await editorPage.makeHTTPRequest('DELETE', `${TEST_API_URL}/api/users/${userId}`); - + // Wait for response await page.waitForTimeout(2000); - + // Verify deletion response const content = await editorPage.getEditorContent(); expect(content).toContain('User deleted successfully'); @@ -205,10 +205,10 @@ test.describe('HTTP Request Functionality', () => { test('should handle 404 Not Found error', async ({ page }) => { // Make request to non-existent resource await editorPage.makeHTTPRequest('GET', `${TEST_API_URL}/api/users/999`); - + // Wait for response await page.waitForTimeout(2000); - + // Verify error is shown const content = await editorPage.getEditorContent(); expect(content).toContain('error'); @@ -218,10 +218,10 @@ test.describe('HTTP Request Functionality', () => { test('should handle 400 Bad Request error', async ({ page }) => { // Make request to error endpoint await editorPage.makeHTTPRequest('GET', `${TEST_API_URL}/api/error/400`); - + // Wait for response await page.waitForTimeout(2000); - + // Verify error response const content = await editorPage.getEditorContent(); expect(content).toContain('Bad Request'); @@ -230,10 +230,10 @@ test.describe('HTTP Request Functionality', () => { test('should handle 500 Server Error', async ({ page }) => { // Make request to error endpoint await editorPage.makeHTTPRequest('GET', `${TEST_API_URL}/api/error/500`); - + // Wait for response await page.waitForTimeout(2000); - + // Verify error response const content = await editorPage.getEditorContent(); expect(content).toContain('Internal Server Error'); @@ -242,10 +242,10 @@ test.describe('HTTP Request Functionality', () => { test('should handle network errors gracefully', async ({ page }) => { // Make request to non-existent server await editorPage.makeHTTPRequest('GET', 'http://localhost:9999/api/test'); - + // Wait for error await page.waitForTimeout(2000); - + // Verify error message is shown const errorElement = page.locator('text=/Failed to fetch|Network error|ECONNREFUSED/i'); await expect(errorElement).toBeVisible(); @@ -258,13 +258,13 @@ test.describe('HTTP Request Functionality', () => { await editorPage.addHeader('Authorization', 'Bearer test-token', true); await editorPage.addHeader('X-Custom-Header', 'CustomValue', false); await editorPage.saveRequestSettings(); - + // Make request to headers endpoint await editorPage.makeHTTPRequest('GET', `${TEST_API_URL}/api/headers`); - + // Wait for response await page.waitForTimeout(2000); - + // Verify headers were sent const content = await editorPage.getEditorContent(); expect(content).toContain('authorization'); @@ -281,16 +281,16 @@ test.describe('HTTP Request Functionality', () => { }; await editorPage.setCustomRequestBody(JSON.stringify(customBody)); await editorPage.saveRequestSettings(); - + // Set different data in editor (should be ignored) await editorPage.setEditorContent('{"ignored": "data"}'); - + // Make POST request await editorPage.makeHTTPRequest('POST', `${TEST_API_URL}/api/echo`); - + // Wait for response await page.waitForTimeout(2000); - + // Verify custom body was sent const content = await editorPage.getEditorContent(); const response = JSON.parse(content); @@ -303,25 +303,25 @@ test.describe('HTTP Request Functionality', () => { test('should handle large JSON responses', async ({ page }) => { // Request dataset (reduced to 10 for stability) await editorPage.makeHTTPRequest('GET', `${TEST_API_URL}/api/large?size=10`); - + // Wait for response and rendering await page.waitForTimeout(4000); - + // Check that no error is shown await editorPage.waitForNoErrors(); - + // Verify data is loaded const content = await editorPage.getEditorContent(); console.log('Content length:', content.length); console.log('First 100 chars:', content.substring(0, 100)); - + if (content.length > 0) { // Try to parse JSON - if it's truncated, skip validation try { const json = JSON.parse(content); expect(json.count).toBe(10); expect(json.data.length).toBe(10); - } catch (e) { + } catch { // If JSON is truncated, just check that we got some data console.log('JSON parsing failed, checking for partial content'); expect(content).toContain('"count": 10'); @@ -330,7 +330,7 @@ test.describe('HTTP Request Functionality', () => { } else { throw new Error('No content loaded in editor'); } - + // Verify graph renders (might be limited nodes shown) const nodeCount = await editorPage.getNodeCount(); expect(nodeCount).toBeGreaterThan(0); @@ -339,14 +339,14 @@ test.describe('HTTP Request Functionality', () => { test('should handle slow responses', async ({ page }) => { // Make request with delay await editorPage.makeHTTPRequest('GET', `${TEST_API_URL}/api/slow?delay=2000`); - + // Should show loading state (if implemented) // Wait for response await page.waitForTimeout(4000); - + // Verify response is eventually loaded const content = await editorPage.getEditorContent(); expect(content).toContain('Response delayed by 2000ms'); }); }); -}); \ No newline at end of file +}); diff --git a/tests/e2e/simple-test.spec.ts b/tests/e2e/simple-test.spec.ts index 4911fda..1ede287 100644 --- a/tests/e2e/simple-test.spec.ts +++ b/tests/e2e/simple-test.spec.ts @@ -3,16 +3,16 @@ import { test, expect } from '@playwright/test'; test('PDJSONEditor should load', async ({ page }) => { // Navigate to the app await page.goto('/'); - + // Wait a bit for the app to load await page.waitForTimeout(2000); - + // Check that the page title or header contains JSON Editor const header = page.locator('h1'); await expect(header).toContainText('JSON'); - + // Take a screenshot for debugging await page.screenshot({ path: 'test-results/app-loaded.png' }); - + console.log('✅ App loaded successfully'); -}); \ No newline at end of file +}); diff --git a/tests/e2e/smoke.spec.ts b/tests/e2e/smoke.spec.ts index 8e8c458..92aff27 100644 --- a/tests/e2e/smoke.spec.ts +++ b/tests/e2e/smoke.spec.ts @@ -4,72 +4,75 @@ test.describe('Smoke Test', () => { test('should load the application', async ({ page }) => { // Navigate to the app await page.goto('/'); - + // Wait for the editor to be ready await page.waitForSelector('.cm-editor', { timeout: 10000 }); - + // Check that key elements are visible await expect(page.locator('.cm-editor')).toBeVisible(); await expect(page.locator('.svelte-flow')).toBeVisible(); - + // Check that the header is visible await expect(page.locator('header')).toBeVisible(); - + // Check that Format and Minify buttons exist const formatButton = page.getByRole('button', { name: 'Format' }); const minifyButton = page.getByRole('button', { name: 'Minify' }); - + await expect(formatButton).toBeVisible(); await expect(minifyButton).toBeVisible(); - + // Check that HTTP method dropdown exists - const methodDropdown = page.locator('button').filter({ hasText: /GET|POST|PUT|PATCH|DELETE/ }).first(); + const methodDropdown = page + .locator('button') + .filter({ hasText: /GET|POST|PUT|PATCH|DELETE/ }) + .first(); await expect(methodDropdown).toBeVisible(); - + // Check that URL input exists const urlInput = page.locator('input[type="url"]'); await expect(urlInput).toBeVisible(); - + // Get the editor content and verify it's valid JSON const lines = await page.locator('.cm-line').allTextContents(); const editorContent = lines.join('\n').trimEnd(); expect(() => JSON.parse(editorContent)).not.toThrow(); - + // Verify the initial JSON contains expected data expect(editorContent).toContain('donut'); expect(editorContent).toContain('Cake'); }); - + test('should format and minify JSON', async ({ page }) => { await page.goto('/'); await page.waitForSelector('.cm-editor'); - + // Get initial content let lines = await page.locator('.cm-line').allTextContents(); const initialContent = lines.join('\n').trimEnd(); const initialLineCount = initialContent.split('\n').length; - + // Click Minify await page.getByRole('button', { name: 'Minify' }).click(); await page.waitForTimeout(500); - + // Check that content is minified lines = await page.locator('.cm-line').allTextContents(); const minifiedContent = lines.join('\n').trimEnd(); const minifiedLineCount = minifiedContent.split('\n').length; expect(minifiedLineCount).toBeLessThan(initialLineCount); - + // Click Format await page.getByRole('button', { name: 'Format' }).click(); await page.waitForTimeout(500); - + // Check that content is formatted again lines = await page.locator('.cm-line').allTextContents(); const formattedContent = lines.join('\n').trimEnd(); const formattedLineCount = formattedContent.split('\n').length; expect(formattedLineCount).toBeGreaterThan(minifiedLineCount); - + // Verify JSON is still valid after format/minify cycle expect(() => JSON.parse(formattedContent)).not.toThrow(); }); -}); \ No newline at end of file +});