test(ci): add exporter heading levels and a11y typography smoke; update MAIN_PLAN
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
const builderUrl = '/en/builder'
|
||||
|
||||
test.describe('Builder basic flow', () => {
|
||||
test('add sections, edit via inspector, preview updates', async ({ page }) => {
|
||||
await page.goto(builderUrl)
|
||||
|
||||
await expect(page.getByTestId('builder-shell')).toBeVisible()
|
||||
|
||||
await page.getByTestId('card-hero').click()
|
||||
await page.getByTestId('card-faq').click()
|
||||
await page.getByTestId('card-features').click()
|
||||
await page.getByTestId('card-testimonials').click()
|
||||
await page.getByTestId('card-cta').click()
|
||||
|
||||
// Hero edit
|
||||
await page.getByLabel('Select 0').click()
|
||||
await page.getByLabel('Hero Heading').fill('Hello World')
|
||||
|
||||
// FAQ edit
|
||||
await page.getByLabel('Select 1').click()
|
||||
await page.getByRole('button', { name: 'Add FAQ Item' }).click()
|
||||
await page.getByLabel('FAQ Q 0').fill('What is this?')
|
||||
await page.getByLabel('FAQ A 0').fill('A builder.')
|
||||
|
||||
// Features edit
|
||||
await page.getByLabel('Select 2').click()
|
||||
await page.getByRole('button', { name: 'Add Feature Item' }).click()
|
||||
await page.getByRole('textbox', { name: 'Feature 0' }).fill('Fast setup')
|
||||
|
||||
// Testimonials edit
|
||||
await page.getByLabel('Select 3').click()
|
||||
await page.getByRole('button', { name: 'Add Testimonial' }).click()
|
||||
await page.getByLabel('Author 0').fill('Alice')
|
||||
await page.getByLabel('Quote 0').fill('Great!')
|
||||
|
||||
// CTA edit
|
||||
await page.getByLabel('Select 4').click()
|
||||
await page.getByLabel('CTA Text').fill('Start Now')
|
||||
await page.getByLabel('CTA Href').fill('/start')
|
||||
|
||||
// Preview assertions
|
||||
await expect(page.getByText('• hero — heading: Hello World')).toBeVisible()
|
||||
await expect(page.getByText('• faq — items: 1')).toBeVisible()
|
||||
await expect(page.getByText(/features — items:\s*4/)).toBeVisible()
|
||||
await expect(page.getByText(/testimonials — items:\s*3/)).toBeVisible()
|
||||
await expect(page.getByText('• cta — Start Now → /start')).toBeVisible()
|
||||
|
||||
// Export should not crash
|
||||
await page.getByRole('button', { name: /Export/i }).click()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,7 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
|
||||
describe('e2e placeholder (see .e2e.ts files for Playwright)', () => {
|
||||
it('skips in Vitest environment', () => {
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,32 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
const builderUrl = '/en/builder'
|
||||
|
||||
test('dnd keyboard fallback: Up/Down buttons reorder sections', async ({ page }) => {
|
||||
await page.goto(builderUrl)
|
||||
|
||||
// add specific sections in order: hero, faq, cta
|
||||
await page.getByTestId('card-hero').click()
|
||||
await page.getByTestId('card-faq').click()
|
||||
await page.getByTestId('card-cta').click()
|
||||
|
||||
const list = page.getByTestId('section-canvas')
|
||||
const items = () => list.getByTestId(/sec-item-/)
|
||||
|
||||
// initial order
|
||||
await expect(items().nth(0)).toContainText('hero')
|
||||
await expect(items().nth(1)).toContainText('faq')
|
||||
await expect(items().nth(2)).toContainText('cta')
|
||||
|
||||
// move first item down
|
||||
await items().nth(0).getByLabel('Down 0').click()
|
||||
await expect(items().nth(0)).toContainText('faq')
|
||||
await expect(items().nth(1)).toContainText('hero')
|
||||
await expect(items().nth(2)).toContainText('cta')
|
||||
|
||||
// move last item up
|
||||
await items().nth(2).getByLabel('Up 2').click()
|
||||
await expect(items().nth(0)).toContainText('faq')
|
||||
await expect(items().nth(1)).toContainText('cta')
|
||||
await expect(items().nth(2)).toContainText('hero')
|
||||
})
|
||||
@@ -0,0 +1,75 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import JSZip from 'jszip'
|
||||
|
||||
const builderUrl = '/en/builder'
|
||||
type WindowWithExport = Window & {
|
||||
__captureExport?: boolean
|
||||
__lastExport?: { html: string; css: string; js: string }
|
||||
__lastBlob?: Blob
|
||||
__exportPreview?: () => { html: string; css: string; js: string }
|
||||
}
|
||||
|
||||
test('Export ZIP contains expected SEO/OG/Analytics meta in index.html', async ({ page }) => {
|
||||
await page.goto(builderUrl)
|
||||
// enable capture hook in app
|
||||
await page.evaluate(() => {
|
||||
const w = window as unknown as WindowWithExport
|
||||
w.__captureExport = true
|
||||
w.__lastBlob = undefined
|
||||
})
|
||||
|
||||
// set SEO/Analytics/OG values
|
||||
await page.getByLabel('SEO Title').fill('E2E SEO Title')
|
||||
await page.getByLabel('SEO Description').fill('E2E SEO Description')
|
||||
await page.getByLabel('OG Image URL').fill('https://example.com/e2e-og.png')
|
||||
await page.getByLabel('GA4 Measurement ID').fill('G-E2E1234567')
|
||||
await page.getByLabel('Meta Pixel ID').fill('9876543210')
|
||||
|
||||
// Prefer calling exporter directly for deterministic output
|
||||
const exported = await page.evaluate(() => (window as unknown as WindowWithExport).__exportPreview?.() ?? null)
|
||||
let indexHtml: string | null = exported?.html ?? null
|
||||
|
||||
// Fallback: also click Export to exercise button and capture via test hook
|
||||
if (!indexHtml) {
|
||||
await page.getByRole('button', { name: /Export/i }).click()
|
||||
await page.waitForFunction(() => {
|
||||
const w = window as unknown as WindowWithExport
|
||||
return Boolean(w.__lastExport || w.__lastBlob)
|
||||
}, { timeout: 20000 })
|
||||
const exp2 = await page.evaluate(() => (window as unknown as WindowWithExport).__lastExport ?? null)
|
||||
indexHtml = exp2?.html ?? null
|
||||
}
|
||||
|
||||
// if not present, fall back to reading from ZIP blob (slower)
|
||||
if (!indexHtml) {
|
||||
await page.waitForFunction(() => Boolean((window as unknown as WindowWithExport).__lastBlob), { timeout: 20000 })
|
||||
const base64 = await page.evaluate(async () => {
|
||||
const blob: Blob | undefined = (window as unknown as WindowWithExport).__lastBlob
|
||||
if (!blob) return null
|
||||
const ab = await blob.arrayBuffer()
|
||||
const bytes = new Uint8Array(ab)
|
||||
return btoa(String.fromCharCode(...bytes))
|
||||
})
|
||||
expect(base64, 'export blob should be captured').not.toBeNull()
|
||||
const buf = Buffer.from(base64 as string, 'base64')
|
||||
const zip = await JSZip.loadAsync(buf)
|
||||
const index = zip.file('index.html')
|
||||
expect(index, 'index.html exists in zip').toBeTruthy()
|
||||
indexHtml = await index!.async('string')
|
||||
}
|
||||
// assert we have HTML
|
||||
expect(indexHtml).not.toBeNull()
|
||||
|
||||
// assertions: language, title/description, og tags, GA4 id presence
|
||||
expect(indexHtml!).toContain('<html lang="en">')
|
||||
expect(indexHtml!).toContain('<meta name="description" content="E2E SEO Description" />')
|
||||
expect(indexHtml!).toContain('<meta property="og:title" content="E2E SEO Title" />')
|
||||
expect(indexHtml!).toContain('<meta property="og:description" content="E2E SEO Description" />')
|
||||
expect(indexHtml!).toContain('<meta property="og:image" content="https://example.com/e2e-og.png" />')
|
||||
expect(indexHtml!).toContain('G-E2E1234567')
|
||||
expect(indexHtml!).toContain('9876543210')
|
||||
|
||||
// accessibility scaffolding in exported HTML
|
||||
expect(indexHtml!).toContain('<a href="#main" class="skip-link">')
|
||||
expect(indexHtml!).toContain('<main id="main" role="main">')
|
||||
})
|
||||
@@ -0,0 +1,45 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
const builderUrl = '/en/builder'
|
||||
|
||||
type WindowWithExport = Window & {
|
||||
__exportPreview?: () => { html: string; css: string; js: string }
|
||||
}
|
||||
|
||||
test('Preview/export markers: heading/ARIA/responsive tokens exist', async ({ page }) => {
|
||||
await page.goto(builderUrl)
|
||||
|
||||
// add sections to ensure markers exist in export
|
||||
await page.getByTestId('card-hero').click()
|
||||
await page.getByTestId('card-features').click()
|
||||
await page.getByTestId('card-testimonials').click()
|
||||
await page.getByTestId('card-faq').click()
|
||||
await page.getByTestId('card-cta').click()
|
||||
|
||||
// ensure FAQ has at least one item to satisfy schema
|
||||
await page.getByLabel('Select 3').click()
|
||||
await page.getByRole('button', { name: 'Add FAQ Item' }).click()
|
||||
await page.getByLabel('FAQ Q 0').fill('What?')
|
||||
await page.getByLabel('FAQ A 0').fill('Answer')
|
||||
|
||||
// ensure exporter hook is present
|
||||
await page.waitForFunction(() => Boolean((window as unknown as WindowWithExport).__exportPreview), { timeout: 5000 })
|
||||
const exported = await page.evaluate(() => (window as unknown as WindowWithExport).__exportPreview?.() ?? null)
|
||||
expect(exported).not.toBeNull()
|
||||
|
||||
const html = exported!.html
|
||||
const css = exported!.css
|
||||
|
||||
// headings/aria
|
||||
expect(html).toContain('<h1 id="hero-heading">')
|
||||
expect(html).toContain('aria-labelledby="features-heading"')
|
||||
expect(html).toContain('aria-labelledby="testimonials-heading"')
|
||||
expect(html).toContain('aria-labelledby="faq-heading"')
|
||||
expect(html).toContain('aria-labelledby="cta-heading"')
|
||||
|
||||
// responsive/a11y css tokens
|
||||
expect(css).toMatch(/@media \(max-width: 640px\)/)
|
||||
expect(css).toMatch(/@media \(prefers-reduced-motion: reduce\)/)
|
||||
expect(css).toMatch(/a\.skip-link:focus/)
|
||||
expect(css).toMatch(/outline:\s*3px\s*solid/)
|
||||
})
|
||||
@@ -0,0 +1,19 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
const builderUrl = '/en/builder'
|
||||
|
||||
test('SEO/Analytics inspector updates reflect in preview', async ({ page }) => {
|
||||
await page.goto(builderUrl)
|
||||
|
||||
await page.getByLabel('SEO Title').fill('My SEO Title')
|
||||
await page.getByLabel('SEO Description').fill('My SEO Description')
|
||||
await page.getByLabel('OG Image URL').fill('https://example.com/og.png')
|
||||
await page.getByLabel('GA4 Measurement ID').fill('G-ABC1234567')
|
||||
await page.getByLabel('Meta Pixel ID').fill('1234567890')
|
||||
|
||||
await expect(page.getByTestId('preview-seo-title')).toHaveText('My SEO Title')
|
||||
await expect(page.getByTestId('preview-seo-description')).toHaveText('My SEO Description')
|
||||
await expect(page.getByTestId('preview-og-image')).toHaveText('https://example.com/og.png')
|
||||
await expect(page.getByTestId('preview-ga4')).toHaveText('G-ABC1234567')
|
||||
await expect(page.getByTestId('preview-meta-pixel')).toHaveText('1234567890')
|
||||
})
|
||||
@@ -0,0 +1,22 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
const builderUrl = '/en/builder'
|
||||
|
||||
test('Preview viewport toggle sets aria-pressed and container width', async ({ page }) => {
|
||||
await page.goto(builderUrl)
|
||||
|
||||
const desktopBtn = page.getByRole('button', { name: 'Desktop' })
|
||||
const mobileBtn = page.getByRole('button', { name: 'Mobile' })
|
||||
const container = page.getByTestId('preview-container')
|
||||
|
||||
// default: desktop
|
||||
await expect(desktopBtn).toHaveAttribute('aria-pressed', 'true')
|
||||
await expect(mobileBtn).toHaveAttribute('aria-pressed', 'false')
|
||||
await expect(container).toHaveClass(/w-\[768px\]/)
|
||||
|
||||
// switch to mobile
|
||||
await mobileBtn.click()
|
||||
await expect(desktopBtn).toHaveAttribute('aria-pressed', 'false')
|
||||
await expect(mobileBtn).toHaveAttribute('aria-pressed', 'true')
|
||||
await expect(container).toHaveClass(/w-\[375px\]/)
|
||||
})
|
||||
@@ -0,0 +1,25 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
const builderUrl = '/en/builder'
|
||||
|
||||
test('Preview viewport toggle via keyboard (Tab + Enter/Space)', async ({ page }) => {
|
||||
await page.goto(builderUrl)
|
||||
|
||||
// Tab to first button (Desktop)
|
||||
await page.keyboard.press('Tab')
|
||||
await expect(page.getByRole('button', { name: 'Desktop' })).toBeFocused()
|
||||
|
||||
// Press Space to toggle Mobile (first ensure Desktop is selected, then move to Mobile with Tab)
|
||||
await page.keyboard.press('Tab')
|
||||
await expect(page.getByRole('button', { name: 'Mobile' })).toBeFocused()
|
||||
await page.keyboard.press('Space')
|
||||
await expect(page.getByRole('button', { name: 'Mobile' })).toHaveAttribute('aria-pressed', 'true')
|
||||
|
||||
// Back to Desktop with Shift+Tab and Enter
|
||||
await page.keyboard.down('Shift')
|
||||
await page.keyboard.press('Tab')
|
||||
await page.keyboard.up('Shift')
|
||||
await expect(page.getByRole('button', { name: 'Desktop' })).toBeFocused()
|
||||
await page.keyboard.press('Enter')
|
||||
await expect(page.getByRole('button', { name: 'Desktop' })).toHaveAttribute('aria-pressed', 'true')
|
||||
})
|
||||
Reference in New Issue
Block a user