diff --git a/.gitignore b/.gitignore index 295b77e..6c95176 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ Thumbs.db node_modules/ .next/ dist/ +dist-embed/ build/ __pycache__/ *.pyc diff --git a/app/src/api/__tests__/engine.test.ts b/app/src/api/__tests__/engine.test.ts index 4d75f20..136ec75 100644 --- a/app/src/api/__tests__/engine.test.ts +++ b/app/src/api/__tests__/engine.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { getPresets, traceImage, simplifyVector, exportAsDxf } from '../engine'; +import { getPresets, traceImage, simplifyVector, exportAsDxf, setEngineBaseUrl } from '../engine'; // ---------- helpers ---------- @@ -216,3 +216,75 @@ describe('exportAsDxf', () => { ).rejects.toThrow(/DXF export.*failed.*500/i); }); }); + +// ---------- setEngineBaseUrl ---------- + +describe('setEngineBaseUrl', () => { + afterEach(() => { + // Reset to default after each test so other suites are unaffected + setEngineBaseUrl('/engine'); + }); + + it('changes the base URL used by subsequent API calls', async () => { + globalThis.fetch = mockFetchOk({ presets: {} }); + + setEngineBaseUrl('http://example.com/api'); + await getPresets(); + + const [url] = (globalThis.fetch as ReturnType).mock.calls[0]; + expect(url).toBe('http://example.com/api/presets'); + }); + + it('strips trailing slashes from the provided URL', async () => { + globalThis.fetch = mockFetchOk({ presets: {} }); + + setEngineBaseUrl('http://example.com/api///'); + await getPresets(); + + const [url] = (globalThis.fetch as ReturnType).mock.calls[0]; + expect(url).toBe('http://example.com/api/presets'); + }); + + it('can be reset back to the default /engine path', async () => { + globalThis.fetch = mockFetchOk({ presets: {} }); + + setEngineBaseUrl('http://remote.host/v2'); + setEngineBaseUrl('/engine'); + await getPresets(); + + const [url] = (globalThis.fetch as ReturnType).mock.calls[0]; + expect(url).toBe('/engine/presets'); + }); + + it('affects traceImage calls', async () => { + globalThis.fetch = mockFetchOk({ output: '', format: 'svg', metadata: {} }); + + setEngineBaseUrl('https://kerf.example.com/engine'); + const file = new File(['px'], 'img.png', { type: 'image/png' }); + await traceImage(file, 'sign', {}); + + const [url] = (globalThis.fetch as ReturnType).mock.calls[0]; + expect(url).toBe('https://kerf.example.com/engine/trace'); + }); + + it('affects simplifyVector calls', async () => { + globalThis.fetch = mockFetchOk({ output: '', format: 'svg', metadata: {} }); + + setEngineBaseUrl('https://kerf.example.com/engine'); + const file = new File([''], 'input.svg', { type: 'image/svg+xml' }); + await simplifyVector(file, 2.0); + + const [url] = (globalThis.fetch as ReturnType).mock.calls[0]; + expect(url).toBe('https://kerf.example.com/engine/simplify'); + }); + + it('affects exportAsDxf calls', async () => { + globalThis.fetch = mockFetchBlob('DXF'); + + setEngineBaseUrl('https://kerf.example.com/engine'); + await exportAsDxf('', 'inches', 1.0); + + const [url] = (globalThis.fetch as ReturnType).mock.calls[0]; + expect(url).toBe('https://kerf.example.com/engine/simplify'); + }); +}); diff --git a/examples/embed-demo.html b/examples/embed-demo.html new file mode 100644 index 0000000..e081865 --- /dev/null +++ b/examples/embed-demo.html @@ -0,0 +1,80 @@ + + + + + + Kerf Embed Demo — Style Isolation Test + + + + + + +

🔧 Kerf Embed Demo

+

+ This page uses Comic Sans, a bright yellow background, + magenta text, and neon green buttons. If you can see the Kerf app below + with its own styling (not Comic Sans, not magenta), then + Shadow DOM style isolation is working correctly. +

+ +

Host-page controls (should look garish):

+ + + +
+ +

Embedded Kerf Component:

+ + + + +
+ +

More host content (should still look garish):

+

+ This paragraph proves that host styles persist after the embed. + Everything outside <kerf-embed> should use + Comic Sans and magenta colors. +

+ + + + + + diff --git a/vite.embed.config.ts b/vite.embed.config.ts new file mode 100644 index 0000000..109bb19 --- /dev/null +++ b/vite.embed.config.ts @@ -0,0 +1,40 @@ +/// +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { resolve } from 'path'; + +/** + * Root-level Vite library-mode config for the Web Component. + * + * Mirrors app/vite.embed.config.ts but resolves paths relative to the + * monorepo root so `npx vite build --config vite.embed.config.ts` works + * from the project root directory. + */ +export default defineConfig({ + root: resolve(__dirname, 'app'), + plugins: [react()], + build: { + outDir: resolve(__dirname, 'dist-embed'), + emptyOutDir: true, + lib: { + entry: resolve(__dirname, 'app/src/embed.tsx'), + name: 'KerfEmbed', + formats: ['es', 'iife'], + fileName: (format) => { + if (format === 'es') return 'kerf-embed.js'; + return 'kerf-embed.iife.js'; + }, + }, + cssCodeSplit: false, + rollupOptions: { + external: [], + output: { + codeSplitting: false, + assetFileNames: (assetInfo) => { + if (assetInfo.names?.[0]?.endsWith('.css')) return 'style.css'; + return assetInfo.names?.[0] ?? '[name][extname]'; + }, + }, + }, + }, +});