Files
pdfme/packages/cli/__tests__/examples.test.ts
2026-04-02 17:33:58 +09:00

363 lines
11 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest';
import { mkdirSync, readFileSync, rmSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import examplesCmd from '../src/commands/examples.js';
import {
fetchExampleTemplate,
getExampleManifest,
getExampleTemplateNames,
} from '../src/example-templates.js';
import { OFFICIAL_EXAMPLE_FONT_URLS } from '../src/example-fonts.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
const TMP = join(__dirname, '..', '.test-tmp-examples');
describe('examples command', () => {
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
delete process.env.PDFME_EXAMPLES_BASE_URL;
rmSync(TMP, { recursive: true, force: true });
});
it('fetches manifest.json for --list output', async () => {
const fetchMock = vi.fn(async (input: string | URL | Request) => {
expect(String(input)).toBe('https://playground.pdfme.com/template-assets/manifest.json');
return new Response(
JSON.stringify({
schemaVersion: 1,
cliVersion: '0.1.0-alpha.0',
templates: [{ name: 'zeta' }, { name: 'alpha' }],
}),
{
status: 200,
headers: { 'content-type': 'application/json' },
},
);
});
vi.stubGlobal('fetch', fetchMock);
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
await examplesCmd.run!({
args: { list: true, name: undefined, json: false },
rawArgs: [],
cmd: examplesCmd,
} as never);
expect(logSpy.mock.calls.map(([message]) => message)).toEqual([
'Available templates:',
' alpha',
' zeta',
]);
});
it('falls back to index.json when manifest.json is unavailable', async () => {
process.env.PDFME_EXAMPLES_BASE_URL = 'https://fixtures.example.com/template-assets';
vi.stubGlobal(
'fetch',
vi.fn(async (input: string | URL | Request) => {
const url = String(input);
if (url.endsWith('/manifest.json')) {
return new Response('missing', { status: 404 });
}
if (url.endsWith('/index.json')) {
return new Response(JSON.stringify([{ name: 'invoice', author: 'pdfme' }]), {
status: 200,
headers: { 'content-type': 'application/json' },
});
}
throw new Error(`Unexpected URL: ${url}`);
}),
);
const manifest = await getExampleManifest();
expect(manifest.source).toBe('remote');
expect(manifest.url).toBe('https://fixtures.example.com/template-assets/index.json');
expect(manifest.manifest.templates).toEqual([
{
name: 'invoice',
author: 'pdfme',
path: 'invoice/template.json',
thumbnailPath: 'invoice/thumbnail.png',
pageCount: 0,
fieldCount: 0,
schemaTypes: [],
fontNames: [],
hasCJK: false,
basePdfKind: 'unknown',
},
]);
const names = await getExampleTemplateNames();
expect(names).toEqual(['invoice']);
});
it('normalizes manifest entries to a complete metadata shape', async () => {
vi.stubGlobal(
'fetch',
vi.fn(async () =>
new Response(
JSON.stringify({
schemaVersion: 1,
cliVersion: '0.1.0-alpha.0',
templates: [{ name: 'invoice' }],
}),
{
status: 200,
headers: { 'content-type': 'application/json' },
},
),
),
);
const manifest = await getExampleManifest();
expect(manifest.manifest.templates).toEqual([
{
name: 'invoice',
author: 'pdfme',
path: 'invoice/template.json',
thumbnailPath: 'invoice/thumbnail.png',
pageCount: 0,
fieldCount: 0,
schemaTypes: [],
fontNames: [],
hasCJK: false,
basePdfKind: 'unknown',
},
]);
});
it('fetches a template referenced from the manifest', async () => {
process.env.PDFME_EXAMPLES_BASE_URL = 'https://fixtures.example.com/template-assets';
vi.stubGlobal(
'fetch',
vi.fn(async (input: string | URL | Request) => {
const url = String(input);
if (url.endsWith('/manifest.json')) {
return new Response(
JSON.stringify({
schemaVersion: 1,
cliVersion: '0.1.0-alpha.0',
templates: [{ name: 'invoice', path: 'invoice/template.json' }],
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
if (url.endsWith('/invoice/template.json')) {
return new Response(
JSON.stringify({
basePdf: { width: 210, height: 297, padding: [20, 20, 20, 20] },
schemas: [[{ name: 'title', type: 'text' }]],
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
throw new Error(`Unexpected URL: ${url}`);
}),
);
const template = await fetchExampleTemplate('invoice');
expect(template).toEqual({
basePdf: { width: 210, height: 297, padding: [20, 20, 20, 20] },
schemas: [[{ name: 'title', type: 'text' }]],
});
});
it('writes output files and emits structured JSON', async () => {
process.env.PDFME_EXAMPLES_BASE_URL = 'https://fixtures.example.com/template-assets';
mkdirSync(TMP, { recursive: true });
vi.stubGlobal(
'fetch',
vi.fn(async (input: string | URL | Request) => {
const url = String(input);
if (url.endsWith('/manifest.json')) {
return new Response(
JSON.stringify({
schemaVersion: 1,
cliVersion: '0.1.0-alpha.0',
templates: [{ name: 'invoice', path: 'invoice/template.json' }],
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
if (url.endsWith('/invoice/template.json')) {
return new Response(
JSON.stringify({
basePdf: { width: 210, height: 297, padding: [20, 20, 20, 20] },
schemas: [[{ name: 'title', type: 'text' }]],
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
throw new Error(`Unexpected URL: ${url}`);
}),
);
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
const outputPath = join(TMP, 'job.json');
await examplesCmd.run!({
args: {
list: false,
name: 'invoice',
output: outputPath,
withInputs: true,
json: true,
},
rawArgs: [],
cmd: examplesCmd,
} as never);
const payload = JSON.parse(String(logSpy.mock.calls.at(-1)?.[0] ?? 'null'));
expect(payload.ok).toBe(true);
expect(payload.source).toBe('remote');
expect(payload.outputPath).toBe(outputPath);
const written = JSON.parse(readFileSync(outputPath, 'utf8'));
expect(written.inputs).toEqual([{ title: 'Sample title' }]);
});
it('includes editable fields from every template page in generated sample inputs', async () => {
process.env.PDFME_EXAMPLES_BASE_URL = 'https://fixtures.example.com/template-assets';
mkdirSync(TMP, { recursive: true });
vi.stubGlobal(
'fetch',
vi.fn(async (input: string | URL | Request) => {
const url = String(input);
if (url.endsWith('/manifest.json')) {
return new Response(
JSON.stringify({
schemaVersion: 1,
cliVersion: '0.1.0-alpha.0',
templates: [{ name: 'multi-page', path: 'multi-page/template.json' }],
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
if (url.endsWith('/multi-page/template.json')) {
return new Response(
JSON.stringify({
basePdf: { width: 210, height: 297, padding: [20, 20, 20, 20] },
schemas: [
[
{ name: 'page1', type: 'text', content: 'Page 1' },
{ name: 'readonly', type: 'text', content: 'Locked', readOnly: true },
],
{
page2: { type: 'text', content: 'Page 2' },
page2Readonly: { type: 'text', content: 'Locked 2', readOnly: true },
},
[{ name: 'page3', type: 'text' }],
],
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
throw new Error(`Unexpected URL: ${url}`);
}),
);
const outputPath = join(TMP, 'multi-page-job.json');
await examplesCmd.run!({
args: {
list: false,
name: 'multi-page',
output: outputPath,
withInputs: true,
json: true,
},
rawArgs: [],
cmd: examplesCmd,
} as never);
const written = JSON.parse(readFileSync(outputPath, 'utf8'));
expect(written.inputs).toEqual([{ page1: 'Page 1', page2: 'Page 2', page3: 'Sample page3' }]);
});
it('embeds official example font URLs into unified jobs', async () => {
process.env.PDFME_EXAMPLES_BASE_URL = 'https://fixtures.example.com/template-assets';
mkdirSync(TMP, { recursive: true });
vi.stubGlobal(
'fetch',
vi.fn(async (input: string | URL | Request) => {
const url = String(input);
if (url.endsWith('/manifest.json')) {
return new Response(
JSON.stringify({
schemaVersion: 1,
cliVersion: '0.1.0-alpha.0',
templates: [{ name: 'certificate-black', path: 'certificate-black/template.json' }],
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
if (url.endsWith('/certificate-black/template.json')) {
return new Response(
JSON.stringify({
basePdf: { width: 210, height: 297, padding: [20, 20, 20, 20] },
schemas: [[
{
name: 'signature',
type: 'text',
fontName: 'PinyonScript-Regular',
position: { x: 20, y: 20 },
width: 100,
height: 20,
},
]],
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
throw new Error(`Unexpected URL: ${url}`);
}),
);
const outputPath = join(TMP, 'certificate-job.json');
await examplesCmd.run!({
args: {
list: false,
name: 'certificate-black',
output: outputPath,
withInputs: true,
json: true,
},
rawArgs: [],
cmd: examplesCmd,
} as never);
const written = JSON.parse(readFileSync(outputPath, 'utf8'));
expect(written.options.font).toEqual({
'PinyonScript-Regular': {
data: OFFICIAL_EXAMPLE_FONT_URLS['PinyonScript-Regular'],
fallback: false,
subset: true,
},
});
});
});