diff --git a/.gitea/workflows/auto-pr.yml b/.gitea/workflows/auto-pr.yml index b9d12bd..43bd478 100644 --- a/.gitea/workflows/auto-pr.yml +++ b/.gitea/workflows/auto-pr.yml @@ -55,7 +55,7 @@ jobs: done if [ -n "${PR_NUMBER:-}" ]; then - echo "Resolving existing label id for 'automerge' (no creation)" + echo "Resolving existing label id for 'automerge' in $OWNER/$NAME (no creation)" curl -sS -o /tmp/labels.json -H "Authorization: token ${CI_TOKEN}" \ "${GITEA_BASE_URL}/api/v1/repos/${OWNER}/${NAME}/labels?limit=1000" || true LID=$(tr '\n' ' ' < /tmp/labels.json | \ diff --git a/lib/exporter/zip.validation.integrated.test.ts b/lib/exporter/zip.validation.integrated.test.ts new file mode 100644 index 0000000..0244912 --- /dev/null +++ b/lib/exporter/zip.validation.integrated.test.ts @@ -0,0 +1,53 @@ +import { describe, it, expect } from 'vitest' +import JSZip from 'jszip' +import { exportPage } from '@/lib/exporter/html' +import { buildZip } from '@/lib/exporter/zip' +import { AssetManager } from '@/lib/assets/assetManager' +import { pageSchema, type Page } from '@/lib/schema/page' + +function makePageWithLocalAsset(): { page: Page; am: AssetManager } { + const am = new AssetManager({ maxBytes: 1024 * 1024, allowedExts: ['png','svg','jpg','jpeg','webp'] }) + // simulate adding a local image and referencing it by original name via local: prefix + const bytes = new Uint8Array([7,8,9]) + // add as "hero.png" + am.add({ name: 'hero.png', type: 'image/png', bytes }) + + const base: Page = pageSchema.parse({ + title: 'Integrated ZIP', + locale: 'en', + theme: { primaryColor: '#2563eb', fontFamily: 'Inter' }, + sections: [ + { type: 'hero', props: { heading: 'Hello', subheading: 'World', imageUrl: 'local:hero.png' } }, + { type: 'cta', props: { text: 'Go', href: '#' } }, + ], + form: { actionUrl: '', fields: [], spamProtection: { honeypotFieldName: '_hp', minSubmitSeconds: 2 } }, + analytics: { ga4MeasurementId: 'G-TEST', metaPixelId: '', customHeadHtml: '' }, + seo: { title: 't', description: 'd' }, + }) + return { page: base, am } +} + +describe('Export ZIP 통합 검증', () => { + it('exportPage 출력 + buildZip 결과에서 assets 경로/메타가 보존된다', async () => { + const { page, am } = makePageWithLocalAsset() + const out = exportPage(page, { assetManager: am }) + const assets = am.toZipStructure() + const zipBytes = await buildZip({ html: out.html, css: out.css, js: out.js, assets }) + + const zip = await JSZip.loadAsync(zipBytes) + const names = Object.keys(zip.files) + // assets 파일이 존재해야 함 + const assetKey = Object.keys(assets)[0] + expect(names).toContain(assetKey) + + // index.html에 canonical/meta/ga4 스니펫 보존 + const html = await zip.files['index.html'].async('string') + expect(html).toMatch(/]*src="([^"]+)"/) + expect(match && match[1]).toBe(assetKey) + }) +}) diff --git a/lib/exporter/zip.validation.meta.test.ts b/lib/exporter/zip.validation.meta.test.ts new file mode 100644 index 0000000..5934287 --- /dev/null +++ b/lib/exporter/zip.validation.meta.test.ts @@ -0,0 +1,28 @@ +import { describe, it, expect } from 'vitest' +import JSZip from 'jszip' +import { buildZip } from '@/lib/exporter/zip' + +describe('Export ZIP 메타 보존 검증', () => { + it('index.html의 SEO/OG/Analytics 메타가 그대로 보존된다', async () => { + const html = ` + + + + + + + ok` + const css = 'body{color:#222}' + const js = 'console.log("ok")' + + const zipBytes = await buildZip({ html, css, js, assets: {} }) + const zip = await JSZip.loadAsync(zipBytes) + const htmlContent = await zip.files['index.html'].async('string') + + expect(htmlContent).toMatch(/ { + it('assets가 비어있을 때 index.html/styles.css/app.js만 포함한다', async () => { + const zipBytes = await buildZip({ + html: 'aok', + css: 'body{color:#111}', + js: 'console.log("ok")', + assets: {}, + }) + const zip = await JSZip.loadAsync(zipBytes) + const names = Object.keys(zip.files).sort() + expect(names).toEqual(['app.js','index.html','styles.css']) + }) + + it('assets가 있으면 모든 파일을 그대로 포함한다(서브폴더 포함)', async () => { + const assets = { + 'assets/img/logo.png': new Uint8Array([0,1,2,3]), + 'assets/svg/icon.svg': TE.encode(''), + 'assets/fonts/inter.woff2': new Uint8Array([4,5,6]), + } + const zipBytes = await buildZip({ + html: 'aok', + css: 'body{color:#111}', + js: 'console.log("ok")', + assets, + }) + const zip = await JSZip.loadAsync(zipBytes) + const names = Object.keys(zip.files) + expect(names).toContain('index.html') + expect(names).toContain('styles.css') + expect(names).toContain('app.js') + expect(names).toContain('assets/img/logo.png') + expect(names).toContain('assets/svg/icon.svg') + expect(names).toContain('assets/fonts/inter.woff2') + + const logo = await zip.files['assets/img/logo.png'].async('uint8array') + expect(Array.from(logo)).toEqual([0,1,2,3]) + }) +})