diff --git a/package-lock.json b/package-lock.json index c194959..72f5421 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "snapapi", - "version": "0.8.0", + "version": "0.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "snapapi", - "version": "0.8.0", + "version": "0.9.0", "dependencies": { "compression": "^1.8.1", "express": "^4.21.0", diff --git a/src/docs/__tests__/openapi.test.ts b/src/docs/__tests__/openapi.test.ts index 7a93ace..0f02d5f 100644 --- a/src/docs/__tests__/openapi.test.ts +++ b/src/docs/__tests__/openapi.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect } from 'vitest' import { openapiSpec } from '../openapi.js' +import packageJson from '../../../package.json' describe('OpenAPI Spec', () => { it('should include GET /v1/screenshot endpoint', () => { @@ -16,4 +17,34 @@ describe('OpenAPI Spec', () => { const signupPath = openapiSpec.paths['/v1/signup/free'] expect(signupPath).toBeUndefined() }) + + // New TDD tests for the issues we need to fix + it('should have version matching package.json', () => { + expect(openapiSpec.info.version).toBe(packageJson.version) + }) + + it('should NOT contain "Signup" tag', () => { + const signupTag = openapiSpec.tags?.find(tag => tag.name === 'Signup') + expect(signupTag).toBeUndefined() + }) + + it('should include cache parameter in POST /v1/screenshot body schema', () => { + const postScreenshot = openapiSpec.paths['/v1/screenshot']?.post + expect(postScreenshot).toBeDefined() + + const requestBody = postScreenshot.requestBody + expect(requestBody).toBeDefined() + expect(requestBody.required).toBe(true) + + const jsonSchema = requestBody.content['application/json']?.schema + expect(jsonSchema).toBeDefined() + expect(jsonSchema.properties).toBeDefined() + + const cacheProperty = jsonSchema.properties.cache + expect(cacheProperty).toBeDefined() + expect(cacheProperty.type).toBe('boolean') + expect(cacheProperty.default).toBe(true) + expect(cacheProperty.description).toContain('bypass') + expect(cacheProperty.description).toContain('cache') + }) }) diff --git a/src/docs/openapi.ts b/src/docs/openapi.ts index 06cf1e5..8c01b0d 100644 --- a/src/docs/openapi.ts +++ b/src/docs/openapi.ts @@ -1,4 +1,12 @@ import swaggerJsdoc from "swagger-jsdoc"; +import { readFileSync } from "fs"; +import { fileURLToPath } from "url"; +import { dirname, join } from "path"; + +// Read version from package.json dynamically +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const packageJson = JSON.parse(readFileSync(join(__dirname, "../../package.json"), "utf-8")); const options: swaggerJsdoc.Options = { definition: { @@ -20,7 +28,7 @@ const options: swaggerJsdoc.Options = { "- 120 requests per minute per IP (global)\n" + "- 5 requests per hour per IP (playground)\n" + "- Monthly screenshot limits based on your plan tier (API)", - version: "0.3.0", + version: packageJson.version, contact: { name: "SnapAPI Support", url: "https://snapapi.eu", @@ -32,7 +40,6 @@ const options: swaggerJsdoc.Options = { tags: [ { name: "Screenshots", description: "Screenshot capture endpoints" }, { name: "Playground", description: "Free demo (no auth, watermarked)" }, - { name: "Signup", description: "Account creation" }, { name: "Billing", description: "Subscription and payment management" }, { name: "Usage", description: "API usage tracking" }, { name: "System", description: "Health and status endpoints" }, diff --git a/src/routes/__tests__/blog.test.ts b/src/routes/__tests__/blog.test.ts index 8b210d3..e75d472 100644 --- a/src/routes/__tests__/blog.test.ts +++ b/src/routes/__tests__/blog.test.ts @@ -57,6 +57,7 @@ describe('Blog Index Page', () => { expect(html).toContain('/blog/why-screenshot-api') expect(html).toContain('/blog/screenshot-api-performance') expect(html).toContain('/blog/automating-og-images') + expect(html).toContain('/blog/dark-mode-screenshots') }) }) @@ -201,6 +202,71 @@ describe('Blog Post: Automating OG Images', () => { }) }) +describe('Blog Post: Dark Mode Screenshots', () => { + const app = createApp() + + it('GET /blog/dark-mode-screenshots.html returns 200', async () => { + const res = await request(app).get('/blog/dark-mode-screenshots.html') + expect(res.status).toBe(200) + expect(res.headers['content-type']).toContain('text/html') + }) + + it('GET /blog/dark-mode-screenshots redirects 301 to .html', async () => { + const res = await request(app).get('/blog/dark-mode-screenshots') + expect(res.status).toBe(301) + expect(res.headers.location).toBe('/blog/dark-mode-screenshots.html') + }) + + it('contains required SEO elements', async () => { + const res = await request(app).get('/blog/dark-mode-screenshots.html') + const html = res.text + expect(html).toMatch(/.+<\/title>/) + expect(html).toMatch(/<meta name="description" content=".+"/) + expect(html).toMatch(/<meta property="og:title"/) + expect(html).toMatch(/<meta property="og:description"/) + expect(html).toMatch(/<link rel="canonical" href="https:\/\/snapapi\.eu\/blog\/dark-mode-screenshots"/) + expect(html).toMatch(/<meta name="twitter:card"/) + }) + + it('contains JSON-LD BlogPosting schema', async () => { + const res = await request(app).get('/blog/dark-mode-screenshots.html') + const html = res.text + expect(html).toContain('"@type":"BlogPosting"') + }) + + it('has substantial content (~1000 words)', async () => { + const res = await request(app).get('/blog/dark-mode-screenshots.html') + const text = res.text.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ') + const wordCount = text.split(' ').filter(w => w.length > 0).length + expect(wordCount).toBeGreaterThan(800) + }) + + it('contains relevant keywords', async () => { + const res = await request(app).get('/blog/dark-mode-screenshots.html') + const html = res.text + expect(html).toContain('dark mode') + expect(html).toContain('darkMode') + expect(html).toContain('screenshot') + }) + + it('contains code examples', async () => { + const res = await request(app).get('/blog/dark-mode-screenshots.html') + const html = res.text + expect(html).toContain('<pre>') + expect(html).toContain('<code>') + expect(html).toContain('curl') + expect(html).toContain('Node.js') + expect(html).toContain('Python') + }) + + it('contains internal links to pricing and docs', async () => { + const res = await request(app).get('/blog/dark-mode-screenshots.html') + const html = res.text + expect(html).toContain('/pricing') + expect(html).toContain('/docs') + }) +}) + describe('Blog Sitemap & Navigation', () => { it('sitemap contains blog URLs', () => { const sitemap = fs.readFileSync(path.join(publicDir, 'sitemap.xml'), 'utf-8') @@ -208,6 +274,7 @@ describe('Blog Sitemap & Navigation', () => { expect(sitemap).toContain('https://snapapi.eu/blog/why-screenshot-api') expect(sitemap).toContain('https://snapapi.eu/blog/screenshot-api-performance') expect(sitemap).toContain('https://snapapi.eu/blog/automating-og-images') + expect(sitemap).toContain('https://snapapi.eu/blog/dark-mode-screenshots') }) it('index.html has Blog link in nav or footer', () => { diff --git a/src/routes/screenshot.ts b/src/routes/screenshot.ts index 2bd2f44..07dc447 100644 --- a/src/routes/screenshot.ts +++ b/src/routes/screenshot.ts @@ -136,6 +136,10 @@ export const screenshotRouter = Router(); * Page load event to wait for before capturing. * "domcontentloaded" (default) is fastest for most pages. * Use "networkidle2" for JS-heavy SPAs that load data after initial render. + * cache: + * type: boolean + * default: true + * description: Set to false to bypass response cache * examples: * simple: * summary: Simple screenshot