From a724a42bb86f216544757da967ba7eec9668aaa9 Mon Sep 17 00:00:00 2001 From: Darshan Date: Sun, 9 Nov 2025 17:30:31 +0530 Subject: [PATCH 01/17] add: tests. --- Dockerfile | 5 +- package.json | 4 +- tests/e2e/endpoints.test.ts | 192 +++++++++++++++++++++++++++ tests/unit/lighthouse.schema.test.ts | 139 +++++++++++++++++++ tests/unit/screenshot.schema.test.ts | 159 ++++++++++++++++++++++ tests/unit/test-page.test.ts | 59 ++++++++ 6 files changed, 556 insertions(+), 2 deletions(-) create mode 100644 tests/e2e/endpoints.test.ts create mode 100644 tests/unit/lighthouse.schema.test.ts create mode 100644 tests/unit/screenshot.schema.test.ts create mode 100644 tests/unit/test-page.test.ts diff --git a/Dockerfile b/Dockerfile index 90616b5..2474cae 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,11 +31,14 @@ ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 \ NODE_ENV=production WORKDIR /app -USER chrome COPY package.json ./ COPY --from=base /app/node_modules ./node_modules COPY src/ ./src/ +RUN chown -R chrome:chrome /app + +USER chrome + ENTRYPOINT ["tini", "--"] CMD ["bun", "run", "src/server.ts"] diff --git a/package.json b/package.json index 3b66e12..bdc6a85 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,9 @@ "format": "biome check --write ./src", "lint": "biome check ./src", "check": "eslint ./src", - "test": "echo \"Error: no test specified\" && exit 1" + "test": "bun test", + "test:e2e": "bun test tests/e2e", + "test:unit": "bun test tests/unit" }, "keywords": [], "author": "", diff --git a/tests/e2e/endpoints.test.ts b/tests/e2e/endpoints.test.ts new file mode 100644 index 0000000..4ae5e9e --- /dev/null +++ b/tests/e2e/endpoints.test.ts @@ -0,0 +1,192 @@ +import { describe, expect, test } from "bun:test"; + +const BASE_URL = process.env.BASE_URL || "http://localhost:3000"; + +describe("E2E Tests - /v1/health", () => { + test("should return ok status when browser is connected", async () => { + const response = await fetch(`${BASE_URL}/v1/health`); + const data = await response.json(); + + expect(response.status).toBe(200); + expect(response.headers.get("Content-Type")).toBe("application/json"); + expect(data).toHaveProperty("status"); + expect(data.status).toBe("ok"); + }); + + test("should reject POST method", async () => { + const response = await fetch(`${BASE_URL}/v1/health`, { + method: "POST", + }); + + expect(response.status).toBe(404); + }); +}); + +describe("E2E Tests - /v1/test", () => { + test("should return HTML test page", async () => { + const response = await fetch(`${BASE_URL}/v1/test`); + const html = await response.text(); + + expect(response.status).toBe(200); + expect(response.headers.get("Content-Type")).toBe("text/html"); + expect(html).toContain(""); + expect(html).toContain("Browser Configuration Test"); + }); + + test("should include timestamp in response", async () => { + const response = await fetch(`${BASE_URL}/v1/test`); + const html = await response.text(); + + expect(html).toContain("Generated:"); + // Check for ISO 8601 date format + expect(html).toMatch(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/); + }); + + test("should reject POST method", async () => { + const response = await fetch(`${BASE_URL}/v1/test`, { + method: "POST", + }); + + expect(response.status).toBe(404); + }); +}); + +describe("E2E Tests - /v1/screenshots", () => { + test("should capture screenshot with minimal input", async () => { + const response = await fetch(`${BASE_URL}/v1/screenshots`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + url: "https://example.com", + }), + }); + + expect(response.status).toBe(200); + expect(response.headers.get("Content-Type")).toBe("image/png"); + + const buffer = await response.arrayBuffer(); + expect(buffer.byteLength).toBeGreaterThan(0); + + // Verify PNG signature + const uint8 = new Uint8Array(buffer); + expect(uint8[0]).toBe(0x89); + expect(uint8[1]).toBe(0x50); + expect(uint8[2]).toBe(0x4e); + expect(uint8[3]).toBe(0x47); + }, 15000); + + test("should capture screenshot with custom viewport", async () => { + const response = await fetch(`${BASE_URL}/v1/screenshots`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + url: "https://example.com", + viewport: { + width: 800, + height: 600, + }, + }), + }); + + expect(response.status).toBe(200); + const buffer = await response.arrayBuffer(); + expect(buffer.byteLength).toBeGreaterThan(0); + }, 15000); + + test("should support jpeg format", async () => { + const response = await fetch(`${BASE_URL}/v1/screenshots`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + url: "https://example.com", + format: "jpeg", + }), + }); + + expect(response.status).toBe(200); + expect(response.headers.get("Content-Type")).toBe("image/jpeg"); + }, 15000); + + test("should reject invalid URL", async () => { + const response = await fetch(`${BASE_URL}/v1/screenshots`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + url: "not-a-url", + }), + }); + + expect(response.status).toBe(400); + const data = await response.json(); + expect(data).toHaveProperty("error"); + }); + + test("should reject GET method", async () => { + const response = await fetch(`${BASE_URL}/v1/screenshots`); + expect(response.status).toBe(404); + }); +}); + +describe("E2E Tests - /v1/reports", () => { + test("should generate lighthouse report", async () => { + const response = await fetch(`${BASE_URL}/v1/reports`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + url: "https://example.com", + viewport: "mobile", + }), + }); + + expect(response.status).toBe(200); + expect(response.headers.get("Content-Type")).toBe("application/json"); + + const data = await response.json(); + expect(data).toBeTruthy(); + }, 30000); + + test("should support desktop viewport", async () => { + const response = await fetch(`${BASE_URL}/v1/reports`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + url: "https://example.com", + viewport: "desktop", + }), + }); + + expect(response.status).toBe(200); + }, 30000); + + test("should reject invalid viewport", async () => { + const response = await fetch(`${BASE_URL}/v1/reports`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + url: "https://example.com", + viewport: "tablet", + }), + }); + + expect(response.status).toBe(400); + const data = await response.json(); + expect(data).toHaveProperty("error"); + }); + + test("should reject GET method", async () => { + const response = await fetch(`${BASE_URL}/v1/reports`); + expect(response.status).toBe(404); + }); +}); + +describe("E2E Tests - 404 handling", () => { + test("should return 404 for unknown routes", async () => { + const response = await fetch(`${BASE_URL}/unknown`); + expect(response.status).toBe(404); + }); + + test("should return 404 for /v1/unknown", async () => { + const response = await fetch(`${BASE_URL}/v1/unknown`); + expect(response.status).toBe(404); + }); +}); diff --git a/tests/unit/lighthouse.schema.test.ts b/tests/unit/lighthouse.schema.test.ts new file mode 100644 index 0000000..ed13979 --- /dev/null +++ b/tests/unit/lighthouse.schema.test.ts @@ -0,0 +1,139 @@ +import { describe, expect, test } from "bun:test"; +import { lighthouseSchema } from "../../src/schemas"; + +describe("lighthouseSchema", () => { + test("should validate minimal valid input", () => { + const input = { + url: "https://example.com", + viewport: "mobile", + }; + + const result = lighthouseSchema.safeParse(input); + expect(result.success).toBe(true); + }); + + test("should apply default values", () => { + const input = { + url: "https://example.com", + viewport: "mobile", + }; + + const result = lighthouseSchema.parse(input); + expect(result.theme).toBe("light"); + expect(result.html).toBe(false); + expect(result.json).toBe(true); + expect(result.csv).toBe(false); + expect(result.waitUntil).toBe("domcontentloaded"); + expect(result.timeout).toBe(30000); + }); + + test("should validate both viewport options", () => { + const mobileInput = { + url: "https://example.com", + viewport: "mobile", + }; + + const mobileResult = lighthouseSchema.safeParse(mobileInput); + expect(mobileResult.success).toBe(true); + + const desktopInput = { + url: "https://example.com", + viewport: "desktop", + }; + + const desktopResult = lighthouseSchema.safeParse(desktopInput); + expect(desktopResult.success).toBe(true); + }); + + test("should reject invalid viewport", () => { + const input = { + url: "https://example.com", + viewport: "tablet", + }; + + const result = lighthouseSchema.safeParse(input); + expect(result.success).toBe(false); + }); + + test("should validate report format flags", () => { + const input = { + url: "https://example.com", + viewport: "mobile", + html: true, + json: true, + csv: true, + }; + + const result = lighthouseSchema.safeParse(input); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.html).toBe(true); + expect(result.data.json).toBe(true); + expect(result.data.csv).toBe(true); + } + }); + + test("should validate custom thresholds", () => { + const input = { + url: "https://example.com", + viewport: "mobile", + thresholds: { + performance: 90, + accessibility: 80, + "best-practices": 85, + seo: 75, + pwa: 50, + }, + }; + + const result = lighthouseSchema.safeParse(input); + expect(result.success).toBe(true); + }); + + test("should reject threshold out of range", () => { + const input = { + url: "https://example.com", + viewport: "mobile", + thresholds: { + performance: 101, + }, + }; + + const result = lighthouseSchema.safeParse(input); + expect(result.success).toBe(false); + }); + + test("should validate permissions array", () => { + const input = { + url: "https://example.com", + viewport: "mobile", + permissions: ["geolocation", "notifications"], + }; + + const result = lighthouseSchema.safeParse(input); + expect(result.success).toBe(true); + }); + + test("should validate headers", () => { + const input = { + url: "https://example.com", + viewport: "mobile", + headers: { + "X-Custom-Header": "value", + }, + }; + + const result = lighthouseSchema.safeParse(input); + expect(result.success).toBe(true); + }); + + test("should reject invalid URL", () => { + const input = { + url: "not-a-url", + viewport: "mobile", + }; + + const result = lighthouseSchema.safeParse(input); + expect(result.success).toBe(false); + }); +}); diff --git a/tests/unit/screenshot.schema.test.ts b/tests/unit/screenshot.schema.test.ts new file mode 100644 index 0000000..1531cad --- /dev/null +++ b/tests/unit/screenshot.schema.test.ts @@ -0,0 +1,159 @@ +import { describe, expect, test } from "bun:test"; +import { screenshotSchema } from "../../src/schemas"; + +describe("screenshotSchema", () => { + test("should validate minimal valid input", () => { + const input = { + url: "https://example.com", + }; + + const result = screenshotSchema.safeParse(input); + expect(result.success).toBe(true); + }); + + test("should apply default values", () => { + const input = { + url: "https://example.com", + }; + + const result = screenshotSchema.parse(input); + expect(result.theme).toBe("light"); + expect(result.format).toBe("png"); + expect(result.fullPage).toBe(false); + expect(result.quality).toBe(90); + expect(result.waitUntil).toBe("domcontentloaded"); + expect(result.timeout).toBe(30000); + expect(result.sleep).toBe(3000); + }); + + test("should validate custom viewport", () => { + const input = { + url: "https://example.com", + viewport: { + width: 1920, + height: 1080, + }, + }; + + const result = screenshotSchema.safeParse(input); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.viewport?.width).toBe(1920); + expect(result.data.viewport?.height).toBe(1080); + } + }); + + test("should reject invalid URL", () => { + const input = { + url: "not-a-url", + }; + + const result = screenshotSchema.safeParse(input); + expect(result.success).toBe(false); + }); + + test("should reject invalid theme", () => { + const input = { + url: "https://example.com", + theme: "invalid", + }; + + const result = screenshotSchema.safeParse(input); + expect(result.success).toBe(false); + }); + + test("should reject invalid format", () => { + const input = { + url: "https://example.com", + format: "gif", + }; + + const result = screenshotSchema.safeParse(input); + expect(result.success).toBe(false); + }); + + test("should validate all supported formats", () => { + const formats = ["png", "jpeg", "webp"]; + + for (const format of formats) { + const input = { + url: "https://example.com", + format, + }; + + const result = screenshotSchema.safeParse(input); + expect(result.success).toBe(true); + } + }); + + test("should validate quality range for jpeg", () => { + const validInput = { + url: "https://example.com", + format: "jpeg", + quality: 90, + }; + + const result = screenshotSchema.safeParse(validInput); + expect(result.success).toBe(true); + + const invalidInput = { + url: "https://example.com", + format: "jpeg", + quality: 101, + }; + + const invalidResult = screenshotSchema.safeParse(invalidInput); + expect(invalidResult.success).toBe(false); + }); + + test("should validate permissions array", () => { + const input = { + url: "https://example.com", + permissions: ["geolocation", "notifications"], + }; + + const result = screenshotSchema.safeParse(input); + expect(result.success).toBe(true); + }); + + test("should validate geolocation", () => { + const input = { + url: "https://example.com", + geolocation: { + latitude: 37.7749, + longitude: -122.4194, + }, + }; + + const result = screenshotSchema.safeParse(input); + expect(result.success).toBe(true); + }); + + test("should validate clip region", () => { + const input = { + url: "https://example.com", + clip: { + x: 0, + y: 0, + width: 800, + height: 600, + }, + }; + + const result = screenshotSchema.safeParse(input); + expect(result.success).toBe(true); + }); + + test("should validate headers", () => { + const input = { + url: "https://example.com", + headers: { + "X-Custom-Header": "value", + Authorization: "Bearer token", + }, + }; + + const result = screenshotSchema.safeParse(input); + expect(result.success).toBe(true); + }); +}); diff --git a/tests/unit/test-page.test.ts b/tests/unit/test-page.test.ts new file mode 100644 index 0000000..82ffa19 --- /dev/null +++ b/tests/unit/test-page.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, test } from "bun:test"; +import { generateTestHTML } from "../../src/utils/test-page.js"; + +describe("generateTestHTML", () => { + test("should generate valid HTML", () => { + const timestamp = "2024-01-01T00:00:00.000Z"; + const html = generateTestHTML(timestamp); + + expect(html).toContain(""); + expect(html).toContain(""); + expect(html).toContain(""); + }); + + test("should include the timestamp", () => { + const timestamp = "2024-01-01T12:34:56.789Z"; + const html = generateTestHTML(timestamp); + + expect(html).toContain(timestamp); + }); + + test("should include the title", () => { + const timestamp = "2024-01-01T00:00:00.000Z"; + const html = generateTestHTML(timestamp); + + expect(html).toContain("Browser Configuration Test"); + }); + + test("should include viewport section", () => { + const timestamp = "2024-01-01T00:00:00.000Z"; + const html = generateTestHTML(timestamp); + + expect(html).toContain("Viewport Width"); + expect(html).toContain("Viewport Height"); + expect(html).toContain("Device Pixel Ratio"); + }); + + test("should include localization section", () => { + const timestamp = "2024-01-01T00:00:00.000Z"; + const html = generateTestHTML(timestamp); + + expect(html).toContain("Language"); + expect(html).toContain("Timezone"); + }); + + test("should include updateValues function", () => { + const timestamp = "2024-01-01T00:00:00.000Z"; + const html = generateTestHTML(timestamp); + + expect(html).toContain("function updateValues()"); + }); + + test("should include refresh button", () => { + const timestamp = "2024-01-01T00:00:00.000Z"; + const html = generateTestHTML(timestamp); + + expect(html).toContain("refresh-btn"); + expect(html).toContain("🔄 Refresh Values"); + }); +}); From 736bab055e573edb82f8dbf53c39680b503464c9 Mon Sep 17 00:00:00 2001 From: Darshan Date: Sun, 9 Nov 2025 17:33:58 +0530 Subject: [PATCH 02/17] add: folder for ignore. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 3237484..2ecfdd2 100644 --- a/.gitignore +++ b/.gitignore @@ -144,3 +144,4 @@ dist # Test artifacts test-screenshots/ comparison/ +.lighthouse/ From cbd956273e5c515e75a0894c540029538e3724d6 Mon Sep 17 00:00:00 2001 From: Darshan Date: Sun, 9 Nov 2025 17:36:13 +0530 Subject: [PATCH 03/17] update: formatter to include tests as well. --- package.json | 6 +++--- tests/unit/test-page.test.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index bdc6a85..96d71b2 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,9 @@ "dev": "bun --watch src/server.ts", "build": "tsc", "type-check": "tsc --noEmit", - "format": "biome check --write ./src", - "lint": "biome check ./src", - "check": "eslint ./src", + "format": "biome check --write ./src ./tests", + "lint": "biome check ./src ./tests", + "check": "eslint ./src ./tests", "test": "bun test", "test:e2e": "bun test tests/e2e", "test:unit": "bun test tests/unit" diff --git a/tests/unit/test-page.test.ts b/tests/unit/test-page.test.ts index 82ffa19..3dd0f86 100644 --- a/tests/unit/test-page.test.ts +++ b/tests/unit/test-page.test.ts @@ -7,7 +7,7 @@ describe("generateTestHTML", () => { const html = generateTestHTML(timestamp); expect(html).toContain(""); - expect(html).toContain(""); + expect(html).toContain(''); expect(html).toContain(""); }); From a180a3c556a49dd94fb1cf99ae4fb1e1bcea8057 Mon Sep 17 00:00:00 2001 From: Darshan Date: Sun, 9 Nov 2025 17:42:30 +0530 Subject: [PATCH 04/17] ci. --- .github/workflows/check.yml | 19 ++++++++----------- .github/workflows/lint.yml | 19 ++++++++----------- .github/workflows/test.yml | 28 ++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 22 deletions(-) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index f540897..9a2c0e4 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -3,7 +3,7 @@ name: "Check" on: pull_request: push: - branches: [main] + branches: [ main ] jobs: check: @@ -11,18 +11,15 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - - name: Set up Node.js - uses: actions/setup-node@v2 + - name: Setup Bun + uses: oven-sh/setup-bun@v1 with: - node-version: '18' - - - name: Install PNPM - run: npm i -g pnpm + bun-version: 'latest' - name: Install dependencies - run: pnpm install + run: bun install --frozen-lockfile - - name: Format - run: pnpm run check \ No newline at end of file + - name: ESLint check + run: bun run check \ No newline at end of file diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 01e51c9..d23d2b3 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -3,7 +3,7 @@ name: "Lint" on: pull_request: push: - branches: [main] + branches: [ main ] jobs: lint: @@ -11,18 +11,15 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - - name: Set up Node.js - uses: actions/setup-node@v2 + - name: Setup Bun + uses: oven-sh/setup-bun@v1 with: - node-version: '18' - - - name: Install PNPM - run: npm i -g pnpm + bun-version: 'latest' - name: Install dependencies - run: pnpm install + run: bun install --frozen-lockfile - - name: Format - run: pnpm run lint \ No newline at end of file + - name: Biome lint + run: bun run lint \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..ec3e2c7 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,28 @@ +name: "Test" + +on: + pull_request: + push: + branches: [ main ] + +jobs: + tests: + name: "Unit and E2E Tests" + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: 'latest' + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Run unit tests + run: bun test:unit + + - name: Run e2e tests + run: bun test:e2e \ No newline at end of file From 1325bc08e70ab32e1329395f99441e03c99b776b Mon Sep 17 00:00:00 2001 From: Darshan Date: Sun, 9 Nov 2025 17:43:38 +0530 Subject: [PATCH 05/17] update: dockerignore. --- .dockerignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.dockerignore b/.dockerignore index 26396b0..f84fa0c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -15,6 +15,7 @@ LICENSE # Test artifacts test-screenshots/ comparison/ +.lighthouse/ # Build output dist/ From 1c9761f0a9f6bb5f2ef4355d817e17b54f11a366 Mon Sep 17 00:00:00 2001 From: Darshan Date: Sun, 9 Nov 2025 17:44:55 +0530 Subject: [PATCH 06/17] update: start container. --- .github/workflows/test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ec3e2c7..208680f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,5 +24,8 @@ jobs: - name: Run unit tests run: bun test:unit + - name: Start container + run: docker compose up -d + - name: Run e2e tests run: bun test:e2e \ No newline at end of file From a093f682d03383151d91a68dae576f5d73f1ab4f Mon Sep 17 00:00:00 2001 From: Darshan Date: Sun, 9 Nov 2025 17:49:13 +0530 Subject: [PATCH 07/17] wait. --- .github/workflows/test.yml | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 208680f..c47eebb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,5 +27,22 @@ jobs: - name: Start container run: docker compose up -d + - name: Wait for container to be ready + run: | + for i in {1..30}; do + if curl -f http://localhost:3000/v1/health > /dev/null 2>&1; then + echo "Container is ready!" + exit 0 + fi + echo "Waiting for container... ($i/30)" + sleep 1 + done + echo "Container failed to start" + exit 1 + - name: Run e2e tests - run: bun test:e2e \ No newline at end of file + run: bun test:e2e + + - name: Stop container + if: always() + run: docker compose down \ No newline at end of file From 699ab33d20c7445e336a379fcbb5337447b41d2b Mon Sep 17 00:00:00 2001 From: Darshan Date: Sun, 9 Nov 2025 17:51:44 +0530 Subject: [PATCH 08/17] add: logs. --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c47eebb..b9d9e30 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -38,6 +38,8 @@ jobs: sleep 1 done echo "Container failed to start" + echo "Container logs:" + docker compose logs exit 1 - name: Run e2e tests From e401100953d19c400f8762dee24e78886ef55675 Mon Sep 17 00:00:00 2001 From: Darshan Date: Sun, 9 Nov 2025 17:55:48 +0530 Subject: [PATCH 09/17] fix: crash-handler. --- Dockerfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2474cae..424f370 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,8 +17,7 @@ RUN apk upgrade --no-cache --available && \ tini && \ apk add --no-cache font-wqy-zenhei --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community && \ # remove unnecessary chromium files to save space - rm -rf /usr/lib/chromium/chrome_crashpad_handler \ - /usr/lib/chromium/chrome_200_percent.pak \ + rm -rf /usr/lib/chromium/chrome_200_percent.pak \ /usr/lib/chromium/chrome_100_percent.pak \ /usr/lib/chromium/xdg-mime \ /usr/lib/chromium/xdg-settings \ From 118b209113095d98beb7d72e490d0dbc09a1e20a Mon Sep 17 00:00:00 2001 From: Darshan Date: Wed, 12 Nov 2025 12:55:13 +0530 Subject: [PATCH 10/17] address comments. --- package.json | 2 +- src/config/index.ts | 2 +- src/routes/reports.ts | 9 ++++++--- src/routes/screenshots.ts | 9 ++++++--- src/routes/test.ts | 22 +++++++++++++--------- 5 files changed, 27 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 96d71b2..e5636f7 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "keywords": [], "author": "", "license": "ISC", - "packageManager": "pnpm@10.20.0", + "packageManager": "bun@1.3.2", "dependencies": { "lighthouse": "^12.2.1", "playwright-core": "^1.52.0", diff --git a/src/config/index.ts b/src/config/index.ts index fb0a7ca..8128a15 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,4 +1,4 @@ export * from "./browser.js"; export * from "./lighthouse.js"; -export const port = Number(process.env.PORT) || 3000; +export const port = process.env.PORT ? Number(process.env.PORT) : 3000; diff --git a/src/routes/reports.ts b/src/routes/reports.ts index e51eadb..dd8dc01 100755 --- a/src/routes/reports.ts +++ b/src/routes/reports.ts @@ -1,9 +1,11 @@ -import type { BrowserContextOptions } from "playwright-core"; +import type { BrowserContext, BrowserContextOptions } from "playwright-core"; import { playAudit } from "playwright-lighthouse"; import { browser, defaultContext, lighthouseConfigs } from "../config"; import { lighthouseSchema } from "../schemas"; export async function handleReportsRequest(req: Request): Promise { + let context: BrowserContext | undefined; + try { const json = await req.json(); const body = lighthouseSchema.parse(json); @@ -19,7 +21,7 @@ export async function handleReportsRequest(req: Request): Promise { if (body.locale) contextOptions.locale = body.locale; if (body.timezoneId) contextOptions.timezoneId = body.timezoneId; - const context = await browser.newContext(contextOptions); + context = await browser.newContext(contextOptions); // Grant permissions if specified if (body.permissions && body.permissions.length > 0) { @@ -72,7 +74,6 @@ export async function handleReportsRequest(req: Request): Promise { thresholds, }); - await context.close(); const report = Array.isArray(results.report) ? results.report.join("") : results.report; @@ -85,5 +86,7 @@ export async function handleReportsRequest(req: Request): Promise { status: 400, headers: { "Content-Type": "application/json" }, }); + } finally { + await context?.close(); } } diff --git a/src/routes/screenshots.ts b/src/routes/screenshots.ts index 81f5a34..6752612 100755 --- a/src/routes/screenshots.ts +++ b/src/routes/screenshots.ts @@ -1,4 +1,5 @@ import type { + BrowserContext, BrowserContextOptions, PageScreenshotOptions, } from "playwright-core"; @@ -8,6 +9,8 @@ import { screenshotSchema } from "../schemas"; export async function handleScreenshotsRequest( req: Request, ): Promise { + let context: BrowserContext | undefined; + try { const json = await req.json(); const body = screenshotSchema.parse(json); @@ -28,7 +31,7 @@ export async function handleScreenshotsRequest( if (body.timezoneId) contextOptions.timezoneId = body.timezoneId; if (body.geolocation) contextOptions.geolocation = body.geolocation; - const context = await browser.newContext(contextOptions); + context = await browser.newContext(contextOptions); // Grant permissions if specified if (body.permissions && body.permissions.length > 0) { @@ -78,8 +81,6 @@ export async function handleScreenshotsRequest( const screen = await page.screenshot(screenshotOptions); - await context.close(); - return new Response(Buffer.from(screen), { headers: { "Content-Type": `image/${body.format}`, @@ -91,5 +92,7 @@ export async function handleScreenshotsRequest( status: 400, headers: { "Content-Type": "application/json" }, }); + } finally { + await context?.close(); } } diff --git a/src/routes/test.ts b/src/routes/test.ts index 8b1126c..c8c2a1d 100755 --- a/src/routes/test.ts +++ b/src/routes/test.ts @@ -3,17 +3,21 @@ import { generateTestHTML } from "../utils/test-page.js"; export async function handleTestRequest(_req: Request): Promise { const context = await browser.newContext(defaultContext); - const page = await context.newPage(); + try { + const page = await context.newPage(); - await page.goto("about:blank"); + await page.goto("about:blank"); - const html = await page.evaluate(() => { - return new Date().toISOString(); - }); + const html = await page.evaluate(() => { + return new Date().toISOString(); + }); - await context.close(); + await context.close(); - return new Response(generateTestHTML(html), { - headers: { "Content-Type": "text/html" }, - }); + return new Response(generateTestHTML(html), { + headers: { "Content-Type": "text/html" }, + }); + } finally { + await context.close(); + } } From bd5a5f28f2cc572aa6c60ea3cde89f495aa03512 Mon Sep 17 00:00:00 2001 From: Darshan Date: Wed, 12 Nov 2025 12:55:38 +0530 Subject: [PATCH 11/17] ignore test results. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 2ecfdd2..afa4638 100644 --- a/.gitignore +++ b/.gitignore @@ -145,3 +145,4 @@ dist test-screenshots/ comparison/ .lighthouse/ +lighthouse/ From 307c7397e7b475967931fc65ce14f0ae3d0d3737 Mon Sep 17 00:00:00 2001 From: Darshan Date: Wed, 12 Nov 2025 12:57:37 +0530 Subject: [PATCH 12/17] fix: test. --- tests/e2e/endpoints.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/endpoints.test.ts b/tests/e2e/endpoints.test.ts index 4ae5e9e..3441100 100644 --- a/tests/e2e/endpoints.test.ts +++ b/tests/e2e/endpoints.test.ts @@ -10,7 +10,7 @@ describe("E2E Tests - /v1/health", () => { expect(response.status).toBe(200); expect(response.headers.get("Content-Type")).toBe("application/json"); expect(data).toHaveProperty("status"); - expect(data.status).toBe("ok"); + expect(data.status).toBe("pass"); }); test("should reject POST method", async () => { From 6fbdd550efe208e5bedd1acb6f2d0410d7c1e5e6 Mon Sep 17 00:00:00 2001 From: Darshan Date: Wed, 12 Nov 2025 13:29:06 +0530 Subject: [PATCH 13/17] add: more test cases. --- tests/e2e/endpoints.test.ts | 76 +++++++++++++++++++++++++++++++++---- 1 file changed, 69 insertions(+), 7 deletions(-) diff --git a/tests/e2e/endpoints.test.ts b/tests/e2e/endpoints.test.ts index 3441100..617e812 100644 --- a/tests/e2e/endpoints.test.ts +++ b/tests/e2e/endpoints.test.ts @@ -66,13 +66,6 @@ describe("E2E Tests - /v1/screenshots", () => { const buffer = await response.arrayBuffer(); expect(buffer.byteLength).toBeGreaterThan(0); - - // Verify PNG signature - const uint8 = new Uint8Array(buffer); - expect(uint8[0]).toBe(0x89); - expect(uint8[1]).toBe(0x50); - expect(uint8[2]).toBe(0x4e); - expect(uint8[3]).toBe(0x47); }, 15000); test("should capture screenshot with custom viewport", async () => { @@ -107,6 +100,75 @@ describe("E2E Tests - /v1/screenshots", () => { expect(response.headers.get("Content-Type")).toBe("image/jpeg"); }, 15000); + test("should capture full page screenshot", async () => { + const partialResponse = await fetch(`${BASE_URL}/v1/screenshots`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + url: "https://appwrite.io", + fullPage: false, + }), + }); + + const partialBuffer = await partialResponse.arrayBuffer(); + + const fullPageResponse = await fetch(`${BASE_URL}/v1/screenshots`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + url: "https://appwrite.io", + fullPage: true, + }), + }); + + expect(fullPageResponse.status).toBe(200); + const fullPageBuffer = await fullPageResponse.arrayBuffer(); + + // Full page screenshot should be larger than partial + expect(fullPageBuffer.byteLength).toBeGreaterThan(partialBuffer.byteLength); + }, 15000); + + test("should return 400 for malformed JSON", async () => { + const response = await fetch(`${BASE_URL}/v1/screenshots`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: "{ invalid json", + }); + + expect(response.status).toBe(400); + const data = await response.json(); + expect(data).toHaveProperty("error"); + }); + + test("should return 400 for missing URL parameter", async () => { + const response = await fetch(`${BASE_URL}/v1/screenshots`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + format: "png", + }), + }); + + expect(response.status).toBe(400); + const data = await response.json(); + expect(data).toHaveProperty("error"); + }); + + test("should return 400 for invalid image format", async () => { + const response = await fetch(`${BASE_URL}/v1/screenshots`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + format: "webp", + url: "https://example.com", + }), + }); + + expect(response.status).toBe(400); + const data = await response.json(); + expect(data).toHaveProperty("error"); + }); + test("should reject invalid URL", async () => { const response = await fetch(`${BASE_URL}/v1/screenshots`, { method: "POST", From 66d30f4599118487bb6a7d64d7c2bc92170fb8d1 Mon Sep 17 00:00:00 2001 From: Darshan Date: Wed, 12 Nov 2025 13:37:38 +0530 Subject: [PATCH 14/17] add: more test cases. --- tests/e2e/endpoints.test.ts | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/e2e/endpoints.test.ts b/tests/e2e/endpoints.test.ts index 617e812..eda1501 100644 --- a/tests/e2e/endpoints.test.ts +++ b/tests/e2e/endpoints.test.ts @@ -128,6 +128,37 @@ describe("E2E Tests - /v1/screenshots", () => { expect(fullPageBuffer.byteLength).toBeGreaterThan(partialBuffer.byteLength); }, 15000); + test("should capture clipped region", async () => { + const fullResponse = await fetch(`${BASE_URL}/v1/screenshots`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + url: "https://example.com", + }), + }); + + const fullBuffer = await fullResponse.arrayBuffer(); + + const clippedResponse = await fetch(`${BASE_URL}/v1/screenshots`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + url: "https://example.com", + clip: { + x: 0, + y: 0, + width: 400, + height: 300, + }, + }), + }); + + expect(clippedResponse.status).toBe(200); + const clippedBuffer = await clippedResponse.arrayBuffer(); + + expect(clippedBuffer.byteLength).toBeLessThan(fullBuffer.byteLength); + }, 15000); + test("should return 400 for malformed JSON", async () => { const response = await fetch(`${BASE_URL}/v1/screenshots`, { method: "POST", From 81992c26fad7d84e8c605ce7fc5ccef0b7b1ed0b Mon Sep 17 00:00:00 2001 From: Darshan Date: Wed, 12 Nov 2025 13:45:25 +0530 Subject: [PATCH 15/17] add: more unit test cases. --- tests/unit/lighthouse.schema.test.ts | 307 +++++++++++++++++++++++++ tests/unit/screenshot.schema.test.ts | 330 +++++++++++++++++++++++++++ 2 files changed, 637 insertions(+) diff --git a/tests/unit/lighthouse.schema.test.ts b/tests/unit/lighthouse.schema.test.ts index ed13979..07327e8 100644 --- a/tests/unit/lighthouse.schema.test.ts +++ b/tests/unit/lighthouse.schema.test.ts @@ -136,4 +136,311 @@ describe("lighthouseSchema", () => { const result = lighthouseSchema.safeParse(input); expect(result.success).toBe(false); }); + + // Threshold boundary tests + test("should accept thresholds at minimum (0)", () => { + const input = { + url: "https://example.com", + viewport: "mobile", + thresholds: { + performance: 0, + accessibility: 0, + "best-practices": 0, + seo: 0, + pwa: 0, + }, + }; + + const result = lighthouseSchema.safeParse(input); + expect(result.success).toBe(true); + }); + + test("should accept thresholds at maximum (100)", () => { + const input = { + url: "https://example.com", + viewport: "mobile", + thresholds: { + performance: 100, + accessibility: 100, + "best-practices": 100, + seo: 100, + pwa: 100, + }, + }; + + const result = lighthouseSchema.safeParse(input); + expect(result.success).toBe(true); + }); + + test("should reject threshold below minimum", () => { + const input = { + url: "https://example.com", + viewport: "mobile", + thresholds: { + performance: -1, + }, + }; + + const result = lighthouseSchema.safeParse(input); + expect(result.success).toBe(false); + }); + + test("should accept partial thresholds", () => { + const input = { + url: "https://example.com", + viewport: "mobile", + thresholds: { + performance: 90, + }, + }; + + const result = lighthouseSchema.safeParse(input); + expect(result.success).toBe(true); + }); + + test("should reject threshold exceeding maximum for accessibility", () => { + const input = { + url: "https://example.com", + viewport: "mobile", + thresholds: { + accessibility: 101, + }, + }; + + const result = lighthouseSchema.safeParse(input); + expect(result.success).toBe(false); + }); + + test("should reject threshold exceeding maximum for seo", () => { + const input = { + url: "https://example.com", + viewport: "mobile", + thresholds: { + seo: 150, + }, + }; + + const result = lighthouseSchema.safeParse(input); + expect(result.success).toBe(false); + }); + + // Report format combination tests + test("should support json and html formats together", () => { + const input = { + url: "https://example.com", + viewport: "mobile", + json: true, + html: true, + csv: false, + }; + + const result = lighthouseSchema.safeParse(input); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.json).toBe(true); + expect(result.data.html).toBe(true); + expect(result.data.csv).toBe(false); + } + }); + + test("should support json and csv formats together", () => { + const input = { + url: "https://example.com", + viewport: "mobile", + json: true, + html: false, + csv: true, + }; + + const result = lighthouseSchema.safeParse(input); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.json).toBe(true); + expect(result.data.html).toBe(false); + expect(result.data.csv).toBe(true); + } + }); + + test("should support html and csv formats together", () => { + const input = { + url: "https://example.com", + viewport: "mobile", + json: false, + html: true, + csv: true, + }; + + const result = lighthouseSchema.safeParse(input); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.json).toBe(false); + expect(result.data.html).toBe(true); + expect(result.data.csv).toBe(true); + } + }); + + test("should support all formats disabled", () => { + const input = { + url: "https://example.com", + viewport: "mobile", + json: false, + html: false, + csv: false, + }; + + const result = lighthouseSchema.safeParse(input); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.json).toBe(false); + expect(result.data.html).toBe(false); + expect(result.data.csv).toBe(false); + } + }); + + // Timeout boundary tests + test("should accept timeout at minimum (0)", () => { + const input = { + url: "https://example.com", + viewport: "mobile", + timeout: 0, + }; + + const result = lighthouseSchema.safeParse(input); + expect(result.success).toBe(true); + }); + + test("should accept timeout at maximum (120000)", () => { + const input = { + url: "https://example.com", + viewport: "mobile", + timeout: 120000, + }; + + const result = lighthouseSchema.safeParse(input); + expect(result.success).toBe(true); + }); + + test("should reject timeout exceeding maximum", () => { + const input = { + url: "https://example.com", + viewport: "mobile", + timeout: 120001, + }; + + const result = lighthouseSchema.safeParse(input); + expect(result.success).toBe(false); + }); + + test("should reject negative timeout", () => { + const input = { + url: "https://example.com", + viewport: "mobile", + timeout: -1, + }; + + const result = lighthouseSchema.safeParse(input); + expect(result.success).toBe(false); + }); + + // Timezone validation tests + test("should accept valid IANA timezones", () => { + const timezones = [ + "America/New_York", + "Europe/London", + "Asia/Tokyo", + "UTC/GMT", + ]; + + for (const timezoneId of timezones) { + const input = { + url: "https://example.com", + viewport: "mobile", + timezoneId, + }; + + const result = lighthouseSchema.safeParse(input); + expect(result.success).toBe(true); + } + }); + + test("should reject invalid timezone format", () => { + const input = { + url: "https://example.com", + viewport: "mobile", + timezoneId: "InvalidTimezone", + }; + + const result = lighthouseSchema.safeParse(input); + expect(result.success).toBe(false); + }); + + test("should reject timezone without region", () => { + const input = { + url: "https://example.com", + viewport: "mobile", + timezoneId: "America", + }; + + const result = lighthouseSchema.safeParse(input); + expect(result.success).toBe(false); + }); + + // Theme validation tests + test("should accept both theme options", () => { + const lightInput = { + url: "https://example.com", + viewport: "mobile", + theme: "light", + }; + + const lightResult = lighthouseSchema.safeParse(lightInput); + expect(lightResult.success).toBe(true); + + const darkInput = { + url: "https://example.com", + viewport: "mobile", + theme: "dark", + }; + + const darkResult = lighthouseSchema.safeParse(darkInput); + expect(darkResult.success).toBe(true); + }); + + test("should reject invalid theme", () => { + const input = { + url: "https://example.com", + viewport: "mobile", + theme: "blue", + }; + + const result = lighthouseSchema.safeParse(input); + expect(result.success).toBe(false); + }); + + // waitUntil validation tests + test("should accept all valid waitUntil options", () => { + const options = ["load", "domcontentloaded", "networkidle", "commit"]; + + for (const waitUntil of options) { + const input = { + url: "https://example.com", + viewport: "mobile", + waitUntil, + }; + + const result = lighthouseSchema.safeParse(input); + expect(result.success).toBe(true); + } + }); + + test("should reject invalid waitUntil option", () => { + const input = { + url: "https://example.com", + viewport: "mobile", + waitUntil: "complete", + }; + + const result = lighthouseSchema.safeParse(input); + expect(result.success).toBe(false); + }); }); diff --git a/tests/unit/screenshot.schema.test.ts b/tests/unit/screenshot.schema.test.ts index 1531cad..b6082ac 100644 --- a/tests/unit/screenshot.schema.test.ts +++ b/tests/unit/screenshot.schema.test.ts @@ -156,4 +156,334 @@ describe("screenshotSchema", () => { const result = screenshotSchema.safeParse(input); expect(result.success).toBe(true); }); + + // Viewport boundary tests + test("should accept viewport at minimum (1x1)", () => { + const input = { + url: "https://example.com", + viewport: { width: 1, height: 1 }, + }; + + const result = screenshotSchema.safeParse(input); + expect(result.success).toBe(true); + }); + + test("should accept viewport at maximum (3840x2160)", () => { + const input = { + url: "https://example.com", + viewport: { width: 3840, height: 2160 }, + }; + + const result = screenshotSchema.safeParse(input); + expect(result.success).toBe(true); + }); + + test("should reject viewport exceeding maximum width", () => { + const input = { + url: "https://example.com", + viewport: { width: 3841, height: 1080 }, + }; + + const result = screenshotSchema.safeParse(input); + expect(result.success).toBe(false); + }); + + test("should reject viewport exceeding maximum height", () => { + const input = { + url: "https://example.com", + viewport: { width: 1920, height: 2161 }, + }; + + const result = screenshotSchema.safeParse(input); + expect(result.success).toBe(false); + }); + + test("should reject viewport with zero dimensions", () => { + const input = { + url: "https://example.com", + viewport: { width: 0, height: 0 }, + }; + + const result = screenshotSchema.safeParse(input); + expect(result.success).toBe(false); + }); + + // Quality boundary tests + test("should accept quality at minimum (0)", () => { + const input = { + url: "https://example.com", + format: "jpeg", + quality: 0, + }; + + const result = screenshotSchema.safeParse(input); + expect(result.success).toBe(true); + }); + + test("should accept quality at maximum (100)", () => { + const input = { + url: "https://example.com", + format: "jpeg", + quality: 100, + }; + + const result = screenshotSchema.safeParse(input); + expect(result.success).toBe(true); + }); + + test("should reject quality below minimum", () => { + const input = { + url: "https://example.com", + format: "jpeg", + quality: -1, + }; + + const result = screenshotSchema.safeParse(input); + expect(result.success).toBe(false); + }); + + // Sleep boundary tests + test("should accept sleep at minimum (0)", () => { + const input = { + url: "https://example.com", + sleep: 0, + }; + + const result = screenshotSchema.safeParse(input); + expect(result.success).toBe(true); + }); + + test("should accept sleep at maximum (60000)", () => { + const input = { + url: "https://example.com", + sleep: 60000, + }; + + const result = screenshotSchema.safeParse(input); + expect(result.success).toBe(true); + }); + + test("should reject sleep exceeding maximum", () => { + const input = { + url: "https://example.com", + sleep: 60001, + }; + + const result = screenshotSchema.safeParse(input); + expect(result.success).toBe(false); + }); + + test("should reject negative sleep", () => { + const input = { + url: "https://example.com", + sleep: -1, + }; + + const result = screenshotSchema.safeParse(input); + expect(result.success).toBe(false); + }); + + // Timeout boundary tests + test("should accept timeout at minimum (0)", () => { + const input = { + url: "https://example.com", + timeout: 0, + }; + + const result = screenshotSchema.safeParse(input); + expect(result.success).toBe(true); + }); + + test("should accept timeout at maximum (120000)", () => { + const input = { + url: "https://example.com", + timeout: 120000, + }; + + const result = screenshotSchema.safeParse(input); + expect(result.success).toBe(true); + }); + + test("should reject timeout exceeding maximum", () => { + const input = { + url: "https://example.com", + timeout: 120001, + }; + + const result = screenshotSchema.safeParse(input); + expect(result.success).toBe(false); + }); + + // Device scale factor boundary tests + test("should accept device scale factor at minimum (0.1)", () => { + const input = { + url: "https://example.com", + deviceScaleFactor: 0.1, + }; + + const result = screenshotSchema.safeParse(input); + expect(result.success).toBe(true); + }); + + test("should accept device scale factor at maximum (3)", () => { + const input = { + url: "https://example.com", + deviceScaleFactor: 3, + }; + + const result = screenshotSchema.safeParse(input); + expect(result.success).toBe(true); + }); + + test("should reject device scale factor below minimum", () => { + const input = { + url: "https://example.com", + deviceScaleFactor: 0.09, + }; + + const result = screenshotSchema.safeParse(input); + expect(result.success).toBe(false); + }); + + test("should reject device scale factor above maximum", () => { + const input = { + url: "https://example.com", + deviceScaleFactor: 3.1, + }; + + const result = screenshotSchema.safeParse(input); + expect(result.success).toBe(false); + }); + + // Geolocation boundary tests + test("should accept geolocation at latitude minimum (-90)", () => { + const input = { + url: "https://example.com", + geolocation: { latitude: -90, longitude: 0 }, + }; + + const result = screenshotSchema.safeParse(input); + expect(result.success).toBe(true); + }); + + test("should accept geolocation at latitude maximum (90)", () => { + const input = { + url: "https://example.com", + geolocation: { latitude: 90, longitude: 0 }, + }; + + const result = screenshotSchema.safeParse(input); + expect(result.success).toBe(true); + }); + + test("should reject geolocation exceeding latitude bounds", () => { + const input = { + url: "https://example.com", + geolocation: { latitude: 91, longitude: 0 }, + }; + + const result = screenshotSchema.safeParse(input); + expect(result.success).toBe(false); + }); + + test("should accept geolocation at longitude minimum (-180)", () => { + const input = { + url: "https://example.com", + geolocation: { latitude: 0, longitude: -180 }, + }; + + const result = screenshotSchema.safeParse(input); + expect(result.success).toBe(true); + }); + + test("should accept geolocation at longitude maximum (180)", () => { + const input = { + url: "https://example.com", + geolocation: { latitude: 0, longitude: 180 }, + }; + + const result = screenshotSchema.safeParse(input); + expect(result.success).toBe(true); + }); + + test("should reject geolocation exceeding longitude bounds", () => { + const input = { + url: "https://example.com", + geolocation: { latitude: 0, longitude: 181 }, + }; + + const result = screenshotSchema.safeParse(input); + expect(result.success).toBe(false); + }); + + // Clip region boundary tests + test("should accept clip at minimum dimensions", () => { + const input = { + url: "https://example.com", + clip: { x: 0, y: 0, width: 1, height: 1 }, + }; + + const result = screenshotSchema.safeParse(input); + expect(result.success).toBe(true); + }); + + test("should reject clip with zero width", () => { + const input = { + url: "https://example.com", + clip: { x: 0, y: 0, width: 0, height: 100 }, + }; + + const result = screenshotSchema.safeParse(input); + expect(result.success).toBe(false); + }); + + test("should reject clip with zero height", () => { + const input = { + url: "https://example.com", + clip: { x: 0, y: 0, width: 100, height: 0 }, + }; + + const result = screenshotSchema.safeParse(input); + expect(result.success).toBe(false); + }); + + // Timezone validation tests + test("should accept valid IANA timezones", () => { + const timezones = [ + "America/New_York", + "Europe/London", + "Asia/Tokyo", + "UTC/GMT", + ]; + + for (const timezoneId of timezones) { + const input = { + url: "https://example.com", + timezoneId, + }; + + const result = screenshotSchema.safeParse(input); + expect(result.success).toBe(true); + } + }); + + test("should reject invalid timezone format", () => { + const input = { + url: "https://example.com", + timezoneId: "InvalidTimezone", + }; + + const result = screenshotSchema.safeParse(input); + expect(result.success).toBe(false); + }); + + test("should reject timezone without region", () => { + const input = { + url: "https://example.com", + timezoneId: "America", + }; + + const result = screenshotSchema.safeParse(input); + expect(result.success).toBe(false); + }); }); From 5b9501089945e03dc0de983bfe9df54adadf9720 Mon Sep 17 00:00:00 2001 From: Darshan Date: Wed, 12 Nov 2025 13:52:26 +0530 Subject: [PATCH 16/17] use: less heavy site. --- tests/e2e/endpoints.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e/endpoints.test.ts b/tests/e2e/endpoints.test.ts index eda1501..06e9aef 100644 --- a/tests/e2e/endpoints.test.ts +++ b/tests/e2e/endpoints.test.ts @@ -105,7 +105,7 @@ describe("E2E Tests - /v1/screenshots", () => { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - url: "https://appwrite.io", + url: "https://itznotabug.dev", fullPage: false, }), }); @@ -116,7 +116,7 @@ describe("E2E Tests - /v1/screenshots", () => { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - url: "https://appwrite.io", + url: "https://itznotabug.dev", fullPage: true, }), }); From 785ffa58956c123cba6b380b5104875a69c67c87 Mon Sep 17 00:00:00 2001 From: Darshan Date: Thu, 13 Nov 2025 22:11:03 +0530 Subject: [PATCH 17/17] address comment. --- .github/workflows/test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b9d9e30..1ba51aa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,6 +45,6 @@ jobs: - name: Run e2e tests run: bun test:e2e - - name: Stop container - if: always() - run: docker compose down \ No newline at end of file + - name: Print logs + if: failure() + run: docker compose logs \ No newline at end of file