visual testing

This commit is contained in:
2026-04-06 03:17:30 +03:00
parent 53ba44e2f9
commit f04c39cb69
29 changed files with 2575 additions and 76 deletions

View File

@@ -604,11 +604,7 @@ async function downloadPdf() {
const blob = await editorRef.value?.exportPdf()
if (!blob) return
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `${template.value.name || 'belge'}.pdf`
a.click()
URL.revokeObjectURL(url)
window.open(url, '_blank')
} catch (err) {
alert(err instanceof Error ? err.message : 'PDF olusturulamadi')
} finally {
@@ -668,7 +664,7 @@ function resetTemplate() {
<!-- Output -->
<button class="header-btn" :disabled="pdfLoading" @click="downloadPdf">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="1" width="10" height="14" rx="1.5" /><path d="M6 5h4M6 8h4M6 11h2" /></svg>
{{ pdfLoading ? 'Hazirlaniyor...' : 'PDF Indir' }}
{{ pdfLoading ? 'Hazirlaniyor...' : 'PDF Onizle' }}
</button>
</header>
<DreportEditor

View File

@@ -3,12 +3,15 @@ import { computed } from 'vue'
import { useTemplateStore } from '../../stores/template'
import { useEditorStore } from '../../stores/editor'
import { isContainer } from '../../core/types'
import type { ContainerElement, TextStyle, RepeatingTableElement, TableStyle } from '../../core/types'
import type { ElementLayout } from '../../core/layout-types'
import type { ContainerElement, TextStyle, RepeatingTableElement, TableStyle, ChartElement, ChartType } from '../../core/types'
import type { LayoutMapEntry } from '../../core/layout-types'
const PAGE_GAP_PX = 24
const props = defineProps<{
scale: number
layoutMap: Record<string, ElementLayout>
layoutMap: Record<string, LayoutMapEntry>
pageHeightPx?: number
}>()
const templateStore = useTemplateStore()
@@ -36,6 +39,15 @@ const isTable = computed(() => selected.value?.type === 'repeating_table')
const tableEl = computed(() => isTable.value ? selected.value as RepeatingTableElement : null)
const tableStyle = computed(() => tableEl.value?.style as TableStyle | undefined)
const isChart = computed(() => selected.value?.type === 'chart')
const chartEl = computed(() => isChart.value ? selected.value as ChartElement : null)
function pageYOffset(pageIndex: number): number {
if (pageIndex <= 0) return 0
const pageH = props.pageHeightPx ?? (templateStore.template.page.height * props.scale)
return pageIndex * (pageH + PAGE_GAP_PX)
}
const toolbarStyle = computed(() => {
const el = selected.value
if (!el) return { display: 'none' }
@@ -43,10 +55,11 @@ const toolbarStyle = computed(() => {
if (!l) return { display: 'none' }
const s = props.scale
const pYOff = pageYOffset(l.pageIndex)
return {
position: 'absolute' as const,
left: `${l.x_mm * s}px`,
top: `${l.y_mm * s - 30}px`,
top: `${l.y_mm * s - 30 + pYOff}px`,
zIndex: 1100,
}
})
@@ -76,6 +89,13 @@ function updateTableStyle(key: string, value: unknown) {
if (!selected.value) return
update({ style: { ...selected.value.style, [key]: value } })
}
// Chart
function setChartType(t: ChartType) { update({ chartType: t }) }
function updateChartStyle(key: string, value: unknown) {
if (!selected.value) return
update({ style: { ...selected.value.style, [key]: value } })
}
</script>
<template>
@@ -300,6 +320,73 @@ function updateTableStyle(key: string, value: unknown) {
</div>
</template>
<!-- ===== Chart ===== -->
<template v-if="isChart && chartEl">
<!-- Chart type -->
<div class="et__group">
<button class="et__btn" :class="{ 'et__btn--active': chartEl.chartType === 'bar' }" data-tip="Cubuk" @click="setChartType('bar')">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<rect x="2" y="6" width="3" height="6" rx="0.5" fill="currentColor"/>
<rect x="5.5" y="3" width="3" height="9" rx="0.5" fill="currentColor"/>
<rect x="9" y="5" width="3" height="7" rx="0.5" fill="currentColor"/>
</svg>
</button>
<button class="et__btn" :class="{ 'et__btn--active': chartEl.chartType === 'line' }" data-tip="Cizgi" @click="setChartType('line')">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<polyline points="2,10 5,5 8,7 12,3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
<circle cx="2" cy="10" r="1.2" fill="currentColor"/><circle cx="5" cy="5" r="1.2" fill="currentColor"/><circle cx="8" cy="7" r="1.2" fill="currentColor"/><circle cx="12" cy="3" r="1.2" fill="currentColor"/>
</svg>
</button>
<button class="et__btn" :class="{ 'et__btn--active': chartEl.chartType === 'pie' }" data-tip="Pasta" @click="setChartType('pie')">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<path d="M7 2a5 5 0 1 1-3.54 1.46" stroke="currentColor" stroke-width="1.3" fill="none"/>
<path d="M7 7V2A5 5 0 0 0 3.46 3.46Z" fill="currentColor"/>
</svg>
</button>
</div>
<div class="et__sep" />
<!-- Show labels -->
<div class="et__group">
<button class="et__btn" :class="{ 'et__btn--active': chartEl.labels?.show !== false }" data-tip="Etiketler" @click="update({ labels: { ...chartEl.labels, show: chartEl.labels?.show === false ? true : false } })">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<rect x="2" y="8" width="3" height="4" rx="0.5" fill="currentColor" opacity="0.4"/>
<rect x="5.5" y="5" width="3" height="7" rx="0.5" fill="currentColor" opacity="0.4"/>
<rect x="9" y="6" width="3" height="6" rx="0.5" fill="currentColor" opacity="0.4"/>
<text x="3.5" y="7" font-size="4" fill="currentColor" text-anchor="middle" font-weight="bold">3</text>
<text x="7" y="4" font-size="4" fill="currentColor" text-anchor="middle" font-weight="bold">7</text>
<text x="10.5" y="5" font-size="4" fill="currentColor" text-anchor="middle" font-weight="bold">5</text>
</svg>
</button>
</div>
<div class="et__sep" />
<!-- Show grid -->
<div class="et__group">
<button class="et__btn" :class="{ 'et__btn--active': chartEl.axis?.showGrid !== false }" data-tip="Izgara" @click="update({ axis: { ...chartEl.axis, showGrid: chartEl.axis?.showGrid === false ? true : false } })">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<line x1="2" y1="3" x2="12" y2="3" stroke="currentColor" stroke-width="0.8" stroke-dasharray="2 1.5"/>
<line x1="2" y1="7" x2="12" y2="7" stroke="currentColor" stroke-width="0.8" stroke-dasharray="2 1.5"/>
<line x1="2" y1="11" x2="12" y2="11" stroke="currentColor" stroke-width="0.8" stroke-dasharray="2 1.5"/>
</svg>
</button>
</div>
<div class="et__sep" />
<!-- Background color -->
<div class="et__group">
<label class="et__color-wrap" data-tip="Arka Plan">
<input type="color" class="et__color" :value="chartEl.style.backgroundColor ?? '#ffffff'" @input="(e) => updateChartStyle('backgroundColor', (e.target as HTMLInputElement).value)" />
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<rect x="2" y="2" width="10" height="10" rx="1.5" :fill="chartEl.style.backgroundColor ?? '#ffffff'" stroke="#94a3b8" stroke-width="0.8"/>
</svg>
</label>
</div>
</template>
<!-- ===== Line ===== -->
<template v-if="isLine">
<!-- Stroke width -->

View File

@@ -713,6 +713,7 @@ const isAnyDragActive = computed(() =>
v-if="!isDragging && !isResizing"
:scale="scale"
:layout-map="layoutMap"
:page-height-px="pageHeightPx"
/>
</div>
</template>

View File

@@ -0,0 +1,121 @@
<script setup lang="ts">
import { ref, provide, onMounted } from 'vue'
import LayoutRenderer from '../components/editor/LayoutRenderer.vue'
import { useLayoutEngine } from '../composables/useLayoutEngine'
import type { Template } from '../core/types'
// Fixture data is injected by Playwright via window.__DREPORT_FIXTURE__
declare global {
interface Window {
__DREPORT_FIXTURE__?: { template: string; data: string }
}
}
// DPI matching: 150 DPI = 150/25.4 px/mm (must match pdftoppm -r 150)
const DPI = 150
const SCALE = DPI / 25.4
const ready = ref(false)
const errorMsg = ref<string | null>(null)
const template = ref<Template>({
id: 'empty',
name: 'empty',
page: { width: 210, height: 297 },
fonts: ['Noto Sans'],
root: {
id: 'root', type: 'container',
position: { type: 'flow' },
size: { width: { type: 'auto' }, height: { type: 'auto' } },
direction: 'column', gap: 0,
padding: { top: 0, right: 0, bottom: 0, left: 0 },
align: 'stretch', justify: 'start',
style: {}, children: [],
},
})
const data = ref<Record<string, unknown>>({})
const { layout, generateBarcode, error } = useLayoutEngine(template, data)
// Provide barcode generator for LayoutRenderer
provide('generateBarcode', generateBarcode)
// Watch for layout computation to complete
const checkReady = setInterval(() => {
if (layout.value && layout.value.pages.length > 0) {
ready.value = true
clearInterval(checkReady)
}
if (error.value) {
errorMsg.value = error.value
clearInterval(checkReady)
}
}, 50)
// Timeout after 20 seconds
setTimeout(() => {
clearInterval(checkReady)
if (!ready.value) {
errorMsg.value = 'Layout computation timed out'
}
}, 20000)
onMounted(() => {
const fixture = window.__DREPORT_FIXTURE__
if (!fixture) {
errorMsg.value = 'No fixture data found. Set window.__DREPORT_FIXTURE__ = { template, data }'
return
}
try {
template.value = JSON.parse(fixture.template)
data.value = JSON.parse(fixture.data)
} catch (e) {
errorMsg.value = `Failed to parse fixture data: ${e}`
}
})
</script>
<template>
<div
class="render-test-root"
:data-render-ready="ready || undefined"
:data-render-error="errorMsg || undefined"
>
<LayoutRenderer
v-if="layout"
:layout="layout"
:scale="SCALE"
/>
<div v-else-if="errorMsg" class="error">{{ errorMsg }}</div>
<div v-else class="loading">Computing layout...</div>
</div>
</template>
<style>
.render-test-root {
/* No padding/margin — pixel-exact rendering */
background: white;
}
.error {
color: red;
padding: 20px;
font-family: monospace;
}
.loading {
color: #999;
padding: 20px;
font-family: monospace;
}
/* Override layout-page styles for test — no shadow, no margin */
.layout-page {
box-shadow: none !important;
margin: 0 !important;
}
.layout-page + .layout-page {
margin-top: 0 !important;
}
</style>

View File

@@ -0,0 +1,4 @@
import { createApp } from 'vue'
import RenderTestPage from './RenderTestPage.vue'
createApp(RenderTestPage).mount('#app')