Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 39318dd40d | |||
| be80f9339b | |||
| df41aac2b1 | |||
| a620ff970a | |||
| 524abda49c | |||
| fe62656d37 | |||
| 8ad3a9e677 | |||
| e9a87773ea | |||
| 678b081364 | |||
| 46a9c5c83d | |||
| 6c86e1cf85 | |||
| aaea43f2cc | |||
| bc29faf60c | |||
| 26a3b43d28 | |||
| 6ff27295e6 |
@@ -69,7 +69,7 @@ jobs:
|
|||||||
"${GITEA_BASE_URL}/api/v1/repos/${OWNER}/${NAME}/issues/${PR_NUMBER}/labels" \
|
"${GITEA_BASE_URL}/api/v1/repos/${OWNER}/${NAME}/issues/${PR_NUMBER}/labels" \
|
||||||
-d '{"labels":['"${LID}"']}' || true
|
-d '{"labels":['"${LID}"']}' || true
|
||||||
else
|
else
|
||||||
echo "No existing 'automerge' found; skip labeling"
|
echo "No existing 'automerge' label found; skipping label add"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo "PR number could not be determined; skipping label add"
|
echo "PR number could not be determined; skipping label add"
|
||||||
|
|||||||
@@ -39,3 +39,4 @@ yarn-error.log*
|
|||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
MAIN_PLAN.md
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { pageSchema, type Page } from '@/lib/schema/page'
|
|||||||
import { exportPage } from '@/lib/exporter/html'
|
import { exportPage } from '@/lib/exporter/html'
|
||||||
import { AssetManager } from '@/lib/assets/assetManager'
|
import { AssetManager } from '@/lib/assets/assetManager'
|
||||||
import { buildZip } from '@/lib/exporter/zip'
|
import { buildZip } from '@/lib/exporter/zip'
|
||||||
|
import { buildExtrasForPage } from '@/lib/exporter/extras'
|
||||||
import type { Section } from '@/lib/state/store'
|
import type { Section } from '@/lib/state/store'
|
||||||
import { createFormStore, type FormState } from '@/lib/state/formStore'
|
import { createFormStore, type FormState } from '@/lib/state/formStore'
|
||||||
import FormBuilderPanel from '@/components/FormBuilderPanel'
|
import FormBuilderPanel from '@/components/FormBuilderPanel'
|
||||||
@@ -196,7 +197,8 @@ export default function BuilderClientPage() {
|
|||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
const assets = am ? am.toZipStructure() : {}
|
const assets = am ? am.toZipStructure() : {}
|
||||||
const zipBytes = await buildZip({ html: exported.html, css: exported.css, js: exported.js, assets })
|
const extras = buildExtrasForPage(valid)
|
||||||
|
const zipBytes = await buildZip({ html: exported.html, css: exported.css, js: exported.js, assets, extras })
|
||||||
download('landing.zip', zipBytes)
|
download('landing.zip', zipBytes)
|
||||||
}, [pageData, am])
|
}, [pageData, am])
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import type { Page } from '@/lib/schema/page'
|
||||||
|
|
||||||
|
// Sheets 템플릿 등 ZIP 루트/서브 경로에 포함할 추가 파일(extras)을 구성한다.
|
||||||
|
// - actionUrl이 비어있을 때만 Google Apps Script 템플릿을 포함한다.
|
||||||
|
export function buildExtrasForPage(page: Page): Record<string, Uint8Array> {
|
||||||
|
const extras: Record<string, Uint8Array> = {}
|
||||||
|
const te = new TextEncoder()
|
||||||
|
|
||||||
|
const hasAction = !!(page.form?.actionUrl && page.form.actionUrl.trim().length > 0)
|
||||||
|
if (!hasAction) {
|
||||||
|
const code = `// Google Apps Script (Code.gs)
|
||||||
|
// - Web App 으로 배포 후, 아래 doPost 핸들러가 폼 데이터를 수신합니다.
|
||||||
|
// - 시트에 저장하거나 이메일 전송 로직을 추가하세요.
|
||||||
|
function doPost(e) {
|
||||||
|
try {
|
||||||
|
var data = e.parameter || {}
|
||||||
|
// TODO: 시트에 저장 예시
|
||||||
|
// var ss = SpreadsheetApp.getActiveSpreadsheet()
|
||||||
|
// var sheet = ss.getSheetByName('Responses') || ss.insertSheet('Responses')
|
||||||
|
// sheet.appendRow([new Date(), JSON.stringify(data)])
|
||||||
|
return ContentService.createTextOutput(JSON.stringify({ ok: true }))
|
||||||
|
.setMimeType(ContentService.MimeType.JSON)
|
||||||
|
} catch (err) {
|
||||||
|
return ContentService.createTextOutput(JSON.stringify({ ok: false, error: String(err) }))
|
||||||
|
.setMimeType(ContentService.MimeType.JSON)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const readme = `README (Google Sheets Apps Script 템플릿)
|
||||||
|
|
||||||
|
1) Google Drive에서 새 스크립트(Apps Script) 프로젝트를 생성합니다.
|
||||||
|
2) Code.gs 파일 내용을 붙여넣습니다.
|
||||||
|
3) Deploy > New deployment > Web app 으로 배포합니다.
|
||||||
|
- Execute as: Me
|
||||||
|
- Who has access: Anyone
|
||||||
|
4) 배포 URL을 복사하여 빌더의 Form action URL에 넣습니다.
|
||||||
|
5) 필요한 경우 doPost에서 스프레드시트 저장 로직을 추가하세요.
|
||||||
|
`
|
||||||
|
extras['sheets/Code.gs'] = te.encode(code)
|
||||||
|
extras['sheets/README.txt'] = te.encode(readme)
|
||||||
|
}
|
||||||
|
|
||||||
|
return extras
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { exportPage } from '@/lib/exporter/html'
|
||||||
|
import { pageSchema, type Page } from '@/lib/schema/page'
|
||||||
|
|
||||||
|
function makePage(): Page {
|
||||||
|
return pageSchema.parse({
|
||||||
|
title: 'Font Opt',
|
||||||
|
locale: 'en',
|
||||||
|
theme: { primaryColor: '#2563eb', fontFamily: 'Inter' },
|
||||||
|
sections: [ { type: 'hero', props: { heading: 'Hello', subheading: 'World', imageUrl: 'https://img/h.png' } } ],
|
||||||
|
form: { actionUrl: '', fields: [], spamProtection: { honeypotFieldName: '_hp', minSubmitSeconds: 2 } },
|
||||||
|
seo: { title: 't', description: 'd' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Exporter - 폰트 최적화', () => {
|
||||||
|
it('head에 폰트 preconnect 링크가 포함된다(googleapis/gstatic)', () => {
|
||||||
|
const out = exportPage(makePage())
|
||||||
|
const html = out.html
|
||||||
|
expect(html).toMatch(/<link rel="preconnect" href="https:\/\/fonts\.googleapis\.com">/)
|
||||||
|
expect(html).toMatch(/<link rel="preconnect" href="https:\/\/fonts\.gstatic\.com" crossorigin>/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('CSS에 system-ui 폴백이 포함된다', () => {
|
||||||
|
const out = exportPage(makePage())
|
||||||
|
const css = out.css
|
||||||
|
expect(css).toMatch(/font-family:[^;]*system-ui/i)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -336,6 +336,7 @@ export function exportPage(page: Page, opts?: ExportOptions): Exported {
|
|||||||
<html lang="${escapeHtml(page.locale)}">
|
<html lang="${escapeHtml(page.locale)}">
|
||||||
<head>
|
<head>
|
||||||
${buildHead(page)}
|
${buildHead(page)}
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { buildZip } from '@/lib/exporter/zip'
|
||||||
|
import { exportPage } from '@/lib/exporter/html'
|
||||||
|
import { pageSchema, type Page } from '@/lib/schema/page'
|
||||||
|
import { buildExtrasForPage } from '@/lib/exporter/extras'
|
||||||
|
|
||||||
|
function makePageNoAction(): Page {
|
||||||
|
return pageSchema.parse({
|
||||||
|
title: 'Sheets Tpl',
|
||||||
|
locale: 'en',
|
||||||
|
theme: { primaryColor: '#2563eb', fontFamily: 'Inter' },
|
||||||
|
sections: [ { type: 'hero', props: { heading: 'Hello', subheading: 'World', imageUrl: 'https://img/h.png' } } ],
|
||||||
|
form: { actionUrl: '', fields: [ { type: 'text', name: 'email', label: 'Email', required: true } ], spamProtection: { honeypotFieldName: '_hp', minSubmitSeconds: 2 } },
|
||||||
|
seo: { title: 't', description: 'd' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function makePageWithAction(): Page {
|
||||||
|
return pageSchema.parse({
|
||||||
|
title: 'Sheets Off',
|
||||||
|
locale: 'en',
|
||||||
|
theme: { primaryColor: '#2563eb', fontFamily: 'Inter' },
|
||||||
|
sections: [ { type: 'hero', props: { heading: 'Hello', subheading: 'World', imageUrl: 'https://img/h.png' } } ],
|
||||||
|
form: { actionUrl: 'https://example.com/submit', fields: [], spamProtection: { honeypotFieldName: '_hp', minSubmitSeconds: 2 } },
|
||||||
|
seo: { title: 't', description: 'd' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Sheets 템플릿 Export', () => {
|
||||||
|
it('actionUrl 미지정 시 extras에 Sheets 템플릿 파일을 포함한다', async () => {
|
||||||
|
const page = makePageNoAction()
|
||||||
|
const extras = buildExtrasForPage(page)
|
||||||
|
expect(Object.keys(extras)).toContain('sheets/Code.gs')
|
||||||
|
expect(Object.keys(extras)).toContain('sheets/README.txt')
|
||||||
|
|
||||||
|
const out = exportPage(page)
|
||||||
|
const zipBytes = await buildZip({ html: out.html, css: out.css, js: out.js, assets: {}, extras })
|
||||||
|
expect(zipBytes.byteLength).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('actionUrl 지정 시 extras에는 Sheets 템플릿이 포함되지 않는다', () => {
|
||||||
|
const page = makePageWithAction()
|
||||||
|
const extras = buildExtrasForPage(page)
|
||||||
|
expect(Object.keys(extras)).not.toContain('sheets/Code.gs')
|
||||||
|
expect(Object.keys(extras)).not.toContain('sheets/README.txt')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -5,6 +5,8 @@ export type BuildZipInput = {
|
|||||||
css: string
|
css: string
|
||||||
js: string
|
js: string
|
||||||
assets?: Record<string, Uint8Array>
|
assets?: Record<string, Uint8Array>
|
||||||
|
// root-level extra files like favicon.ico, site.webmanifest, robots.txt
|
||||||
|
extras?: Record<string, Uint8Array>
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function buildZip(input: BuildZipInput): Promise<Uint8Array> {
|
export async function buildZip(input: BuildZipInput): Promise<Uint8Array> {
|
||||||
@@ -19,6 +21,22 @@ export async function buildZip(input: BuildZipInput): Promise<Uint8Array> {
|
|||||||
zip.file(path, buf as unknown as Buffer)
|
zip.file(path, buf as unknown as Buffer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (input.extras) {
|
||||||
|
// validate manifest if present
|
||||||
|
if (Object.prototype.hasOwnProperty.call(input.extras, 'site.webmanifest')) {
|
||||||
|
try {
|
||||||
|
const manifestBytes = input.extras['site.webmanifest']
|
||||||
|
const manifestStr = Buffer.from(manifestBytes).toString('utf8')
|
||||||
|
JSON.parse(manifestStr)
|
||||||
|
} catch {
|
||||||
|
throw new Error('Invalid site.webmanifest JSON')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const [path, bytes] of Object.entries(input.extras)) {
|
||||||
|
const buf = Buffer.from(bytes)
|
||||||
|
zip.file(path, buf as unknown as Buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
const content = await zip.generateAsync({ type: 'uint8array' })
|
const content = await zip.generateAsync({ type: 'uint8array' })
|
||||||
return content
|
return content
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(/<link rel="canonical" href="\//)
|
||||||
|
expect(html).toMatch(/<meta name="description" content="d"/)
|
||||||
|
expect(html).toMatch(/gtag\('config', 'G-TEST'\)/)
|
||||||
|
|
||||||
|
// hero 이미지가 재작성된 경로를 사용한다
|
||||||
|
const match = html.match(/<img[^>]*src="([^"]+)"/)
|
||||||
|
expect(match && match[1]).toBe(assetKey)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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 = `<!DOCTYPE html><html><head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="canonical" href="/" />
|
||||||
|
<meta name="description" content="desc" />
|
||||||
|
<meta property="og:title" content="OT" />
|
||||||
|
<meta property="og:description" content="OD" />
|
||||||
|
<script>window.dataLayer=[];function gtag(){dataLayer.push(arguments);}gtag('js',new Date());</script>
|
||||||
|
</head><body>ok</body></html>`
|
||||||
|
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(/<link rel="canonical" href="\//)
|
||||||
|
expect(htmlContent).toMatch(/<meta name="description" content="desc"/)
|
||||||
|
expect(htmlContent).toMatch(/<meta property="og:title" content="OT"/)
|
||||||
|
expect(htmlContent).toMatch(/<meta property="og:description" content="OD"/)
|
||||||
|
expect(htmlContent).toMatch(/window\.dataLayer=\[\];/)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import JSZip from 'jszip'
|
||||||
|
import { buildZip } from '@/lib/exporter/zip'
|
||||||
|
import type { BuildZipInput } from '@/lib/exporter/zip'
|
||||||
|
|
||||||
|
const TE = new TextEncoder()
|
||||||
|
|
||||||
|
describe('Export ZIP 검증 강화 4차: 루트 파일(favicon/manifest/robots)', () => {
|
||||||
|
it('루트 파일들을 포함한다: favicon.ico / site.webmanifest / robots.txt', async () => {
|
||||||
|
const manifest = {
|
||||||
|
name: 'Landing Builder',
|
||||||
|
short_name: 'LB',
|
||||||
|
start_url: '/',
|
||||||
|
display: 'standalone',
|
||||||
|
icons: [{ src: '/favicon.png', sizes: '512x512', type: 'image/png' }],
|
||||||
|
}
|
||||||
|
const input: BuildZipInput = {
|
||||||
|
html: '<!DOCTYPE html><html><head><title>a</title></head><body>ok</body></html>',
|
||||||
|
css: 'body{color:#111}',
|
||||||
|
js: 'console.log("ok")',
|
||||||
|
assets: {},
|
||||||
|
extras: {
|
||||||
|
'favicon.ico': new Uint8Array([0, 1, 2, 3]),
|
||||||
|
'site.webmanifest': TE.encode(JSON.stringify(manifest)),
|
||||||
|
'robots.txt': TE.encode('User-agent: *\nDisallow:'),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const zipBytes = await buildZip(input)
|
||||||
|
|
||||||
|
const zip = await JSZip.loadAsync(zipBytes)
|
||||||
|
const names = Object.keys(zip.files)
|
||||||
|
expect(names).toContain('favicon.ico')
|
||||||
|
expect(names).toContain('site.webmanifest')
|
||||||
|
expect(names).toContain('robots.txt')
|
||||||
|
|
||||||
|
const robots = await zip.files['robots.txt'].async('string')
|
||||||
|
expect(robots).toMatch(/User-agent: \*/)
|
||||||
|
|
||||||
|
const parsed = JSON.parse(await zip.files['site.webmanifest'].async('string'))
|
||||||
|
expect(parsed.name).toBe('Landing Builder')
|
||||||
|
expect(parsed.icons[0].type).toBe('image/png')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('잘못된 manifest JSON이면 실패한다', async () => {
|
||||||
|
const badInput: BuildZipInput = {
|
||||||
|
html: '<!DOCTYPE html><html><head><title>a</title></head><body>ok</body></html>',
|
||||||
|
css: 'body{color:#111}',
|
||||||
|
js: 'console.log("ok")',
|
||||||
|
extras: { 'site.webmanifest': TE.encode('{ invalid json') },
|
||||||
|
}
|
||||||
|
await expect(buildZip(badInput)).rejects.toThrow()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import JSZip from 'jszip'
|
||||||
|
import { buildZip } from '@/lib/exporter/zip'
|
||||||
|
|
||||||
|
const TE = new TextEncoder()
|
||||||
|
|
||||||
|
describe('Export ZIP 검증 강화', () => {
|
||||||
|
it('assets가 비어있을 때 index.html/styles.css/app.js만 포함한다', async () => {
|
||||||
|
const zipBytes = await buildZip({
|
||||||
|
html: '<!DOCTYPE html><html><head><title>a</title></head><body>ok</body></html>',
|
||||||
|
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('<svg></svg>'),
|
||||||
|
'assets/fonts/inter.woff2': new Uint8Array([4,5,6]),
|
||||||
|
}
|
||||||
|
const zipBytes = await buildZip({
|
||||||
|
html: '<!DOCTYPE html><html><head><title>a</title></head><body>ok</body></html>',
|
||||||
|
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])
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user