__STACK__ / HTML + CSS
+__STACK__ / __AUTHORING__
Make the starter
your own.
- Open the complete project, change a template, and watch your PDF take shape. Start with a designed invoice or a three-page report.
+__LEAD__
Edit the full project online
StackBlitz opens the source files and runs the app in your browser. You can try the example without an account or a local Node.js installation.
diff --git a/tools/online_starter.py b/tools/online_starter.py index f84a522..aa94bcc 100644 --- a/tools/online_starter.py +++ b/tools/online_starter.py @@ -5,11 +5,18 @@ STARTERS = { 'browser': {'name': 'JavaScript', 'stack': 'JavaScript + Vite', 'entry': 'src%2Fmain.js', 'run': 'run-the-starter'}, - 'react': {'name': 'React', 'stack': 'React + TypeScript', 'entry': 'src%2FApp.tsx', 'run': 'run-the-project'}, + 'react': {'name': 'React', 'stack': 'React + TypeScript', 'entry': 'src%2Fdocuments%2FInvoice.tsx', 'run': 'run-the-project', + 'authoring': 'PDF components + JSX', + 'description': 'Typed PDF components, editable invoice data and local previews. MIT licensed; fictional sample data.', + 'lead': 'Open the complete project, edit the JSX document and its styles, and watch the PDF update. Start with a component-authored invoice and editable line items.'}, 'vue': {'name': 'Vue', 'stack': 'Vue + TypeScript', 'entry': 'src%2FDocumentEditor.vue', 'run': 'run-the-project'}, } +def starter_description(flavor): + return STARTERS[flavor].get('description', 'Editable HTML/CSS templates and local PDF previews. MIT licensed; fictional sample data.') + + def check_online_links(root, flavor): for page, expected in [('docs/index.md', f'assets/{flavor}-starter/edit-online.html'), (f'docs/guides/{flavor}-pdf.md', f'../assets/{flavor}-starter/edit-online.html')]: @@ -21,7 +28,7 @@ def check_online_links(root, flavor): def write_online_launcher(root, flavor, manifest, project_files): starter = STARTERS[flavor] fields = {'project[title]': f'Fullbleed {starter["name"]} PDF starter', - 'project[description]': 'Editable HTML/CSS templates and local PDF previews. MIT licensed; fictional sample data.', + 'project[description]': starter_description(flavor), 'project[template]': 'node', 'project[dependencies]': '{}'} fields.update({f'project[files][{name}]': value for name, value in sorted(project_files.items())}) inputs = '\n'.join(f'' @@ -30,6 +37,8 @@ def write_online_launcher(root, flavor, manifest, project_files): # Insert project text last so placeholder-like strings inside source files stay intact. for token, value in {'__FLAVOR__': flavor, '__STARTER_NAME__': starter['name'], '__STACK__': starter['stack'], '__ENTRY_FILE__': starter['entry'], '__RUN_SECTION__': starter['run'], + '__AUTHORING__': starter.get('authoring', 'HTML + CSS'), + '__LEAD__': starter.get('lead', 'Open the complete project, change a template, and watch your PDF take shape. Start with a designed invoice or a three-page report.'), '__PACKAGE_VERSION__': manifest['package_version'], '__ENGINE_VERSION__': manifest['engine_version']}.items(): assert token in launcher launcher = launcher.replace(token, escape(value, quote=True)) diff --git a/tools/react_component_checks.py b/tools/react_component_checks.py new file mode 100644 index 0000000..dcbf9a2 --- /dev/null +++ b/tools/react_component_checks.py @@ -0,0 +1,110 @@ +"""Exercise the component-authored invoice in the actual React browser app.""" +from hashlib import sha256 +import json + +from playwright.sync_api import expect +from pypdf import PdfReader + + +def check_components(page, out, baseline, check, ready, download, hold_engine, resume_engine, requested): + records = {row['design']: row for row in json.loads((baseline / 'verification.json').read_text())['records']} + + def compare(name, text): + ready() + row = records[name] + path = download(name, row['pages'], text, normalize_whitespace=True) + check(name + ': matches the installed Node component renderer', path.read_bytes() == (baseline / (name + '.pdf')).read_bytes()) + images = page.locator('#preview img') + check(name + ': every page has a preview', images.count() == row['pages']) + for index in range(images.count()): + value = images.nth(index).evaluate('image => window.pdfBlobBytes(image.src)') + data = bytes(value) + (out / f'{name}-{index + 1}.png').write_bytes(data) + check(name + f': page {index + 1} preview matches Node', sha256(data).hexdigest() == row['previews'][index]) + return path + + ready() + check('components are the initial design', page.locator('#kind').input_value() == 'components') + compare('component-invoice', ['Maple & Finch', 'NS-1042', '8,100.00']) + check('component design needs no HTML editor', page.locator('#html-source').count() == 0) + page.locator('#component-source summary').click() + source = page.locator('#component-source code').last.inner_text() + check('component source shows typed JSX, mapping and conditional content', all(value in source for value in ['@jsxImportSource fullbleed/ui', 'items.map', 'paid &&', 'new StyleSheet'])) + page.locator('#component-source summary').click() + for width, height in [(1440, 1000), (390, 844), (320, 760)]: + page.set_viewport_size({'width': width, 'height': height}) + check(f'components at {width}px: no horizontal overflow', page.evaluate('document.documentElement.scrollWidth <= innerWidth')) + page.screenshot(path=str(out / f'components-{width}.png'), full_page=True) + page.set_viewport_size({'width': 1440, 'height': 1000}) + page.locator('#paid').check() + compare('component-paid', ['PAID IN FULL', 'This invoice has been settled.', '8,100.00']) + page.locator('#paid').uncheck() + ready() + page.locator('#line-items summary').click() + starts = page.evaluate('pdfLifecycle().started') + page.locator('[data-field=quantity]').first.fill('1.5') + expect(page.locator('#status')).to_contain_text('whole hours') + page.wait_for_timeout(650) + check('invalid component data clears stale output without starting a worker', page.locator('#download').is_disabled() and page.locator('#preview img').count() == 0 and page.evaluate('pdfLifecycle().started') == starts) + page.locator('[data-field=quantity]').first.fill('12') + ready() + page.locator('[data-field=rate]').first.fill('12.345') + expect(page.locator('#status')).to_contain_text('two decimal places') + check('invalid cents cannot generate or retain a download', page.locator('#generate').is_disabled() and page.locator('#download').is_disabled()) + page.locator('[data-field=rate]').first.fill('150.00') + ready() + hold_engine() + page.locator('#customer').fill('Obsolete component input') + assert requested.wait(8), 'Expected a held component renderer.' + page.locator('#customer').fill('Birch &