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(/
')
+ expect(html).toContain('')
+ 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