-
Notifications
You must be signed in to change notification settings - Fork 17
fix(caniuse): handle wf- prefixed web-features IDs #494
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
marcoscaceres
wants to merge
13
commits into
main
Choose a base branch
from
fix/caniuse-web-features
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
805a603
fix(caniuse): handle wf- prefixed web-features IDs
marcoscaceres 77bbd62
Apply suggestions from code review
marcoscaceres 7af238d
fix: add explicit format type annotation for TypeScript
marcoscaceres 2c03d80
fix(caniuse): add runtime type guard in getData for CodeQL
marcoscaceres dcb3cd1
fix(caniuse): standardize error responses as JSON
marcoscaceres db579de
Apply suggestions from code review
marcoscaceres 1a77b51
Potential fix for pull request finding 'CodeQL / Reflected cross-site…
marcoscaceres 1c6a717
test(caniuse): add Jasmine tests for getData wf- fallback and feature…
Copilot e13804a
fix(deps): update pnpm-lock.yaml to include escape-html dependency
Copilot 465f76a
Changes before error encountered
Copilot af222f9
Merge branch 'main' into fix/caniuse-web-features
marcoscaceres 742c251
Merge origin/main and revert deprecated caniuse route
Copilot 48c7ce6
Merge origin/main into fix/caniuse-web-features
Copilot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| import os from "node:os"; | ||
| import path from "node:path"; | ||
| import { promises as fs } from "node:fs"; | ||
|
|
||
| import route from "../../../build/routes/caniuse/feature.js"; | ||
| import { cache } from "../../../build/routes/caniuse/lib/index.js"; | ||
|
|
||
| const CANIUSE_DIR = path.join(os.tmpdir(), "caniuse"); | ||
|
|
||
| const FIXTURE = { | ||
| all: { | ||
| chrome: [["100", ["y"]]], | ||
| firefox: [["99", ["n"]]], | ||
| edge: [["100", ["y"]]], | ||
| safari: [["16", ["y"]]], | ||
| and_chr: [["100", ["y"]]], | ||
| and_ff: [["99", ["n"]]], | ||
| ios_saf: [["16", ["y"]]], | ||
| samsung: [["19", ["y"]]], | ||
| }, | ||
| summary: { chrome: [["100", ["y"]]] }, | ||
| }; | ||
|
|
||
| /** Builds a lightweight mock Express Response. */ | ||
| function mockRes() { | ||
| const res = { | ||
| statusCode: 200, | ||
| body: null, | ||
| status(code) { | ||
| this.statusCode = code; | ||
| return this; | ||
| }, | ||
| json(body) { | ||
| this.body = body; | ||
| return this; | ||
| }, | ||
| send(body) { | ||
| this.body = body; | ||
| return this; | ||
| }, | ||
| type(_t) { | ||
| return this; | ||
| }, | ||
| }; | ||
| return res; | ||
| } | ||
|
|
||
| /** Builds a minimal mock Express Request for the `/:feature` route. */ | ||
| function mockReq(feature, query = {}) { | ||
| return { params: { feature }, query }; | ||
| } | ||
|
|
||
| async function writeFixture(name, data = FIXTURE) { | ||
| await fs.mkdir(CANIUSE_DIR, { recursive: true }); | ||
| await fs.writeFile( | ||
| path.join(CANIUSE_DIR, `${name}.json`), | ||
| JSON.stringify(data), | ||
| "utf8", | ||
| ); | ||
| } | ||
|
|
||
| async function removeFixture(name) { | ||
| try { | ||
| await fs.unlink(path.join(CANIUSE_DIR, `${name}.json`)); | ||
| } catch { | ||
| // ignore | ||
| } | ||
| } | ||
|
|
||
| describe("caniuse - feature route", () => { | ||
| beforeEach(() => cache.clear()); | ||
|
|
||
| describe("404 responses", () => { | ||
| it("returns JSON 404 for a missing feature", async () => { | ||
| const res = mockRes(); | ||
| await route(mockReq("nonexistent-xyz"), res); | ||
| expect(res.statusCode).toBe(404); | ||
| expect(res.body).toEqual(jasmine.objectContaining({ error: jasmine.any(String) })); | ||
| expect(res.body.error).toContain("nonexistent-xyz"); | ||
| }); | ||
|
|
||
| it("returns JSON 404 with a wf- hint for a missing wf- feature", async () => { | ||
| const res = mockRes(); | ||
| await route(mockReq("wf-no-such-feature-xyz"), res); | ||
| expect(res.statusCode).toBe(404); | ||
| expect(res.body.error).toContain("wf-"); | ||
| expect(res.body.error).toContain("web-features"); | ||
| }); | ||
|
|
||
| it("returns JSON 404 without wf- hint for the edge case 'wf-'", async () => { | ||
| const res = mockRes(); | ||
| await route(mockReq("wf-"), res); | ||
| expect(res.statusCode).toBe(404); | ||
| expect(res.body.error).not.toContain("web-features"); | ||
| }); | ||
| }); | ||
|
|
||
| describe("successful responses", () => { | ||
| it("returns 200 JSON with browser data for a known feature", async () => { | ||
| await writeFixture("css-grid"); | ||
| try { | ||
| const res = mockRes(); | ||
| await route(mockReq("css-grid"), res); | ||
| expect(res.statusCode).toBe(200); | ||
| expect(res.body).toEqual(jasmine.objectContaining({ result: jasmine.any(Array) })); | ||
| } finally { | ||
| await removeFixture("css-grid"); | ||
| } | ||
| }); | ||
|
|
||
| it("resolves wf- prefixed feature to its caniuse equivalent", async () => { | ||
| await writeFixture("css-grid"); | ||
| try { | ||
| const res = mockRes(); | ||
| await route(mockReq("wf-css-grid"), res); | ||
| expect(res.statusCode).toBe(200); | ||
| expect(res.body.result).toBeDefined(); | ||
| } finally { | ||
| await removeFixture("css-grid"); | ||
| } | ||
| }); | ||
| }); | ||
|
|
||
| describe("500 responses", () => { | ||
| it("returns JSON 500 when the feature file is malformed JSON", async () => { | ||
| await fs.mkdir(CANIUSE_DIR, { recursive: true }); | ||
| await fs.writeFile( | ||
| path.join(CANIUSE_DIR, "broken-feature.json"), | ||
| "not valid json", | ||
| "utf8", | ||
| ); | ||
| try { | ||
| const res = mockRes(); | ||
| await route(mockReq("broken-feature"), res); | ||
| expect(res.statusCode).toBe(500); | ||
| expect(res.body).toEqual(jasmine.objectContaining({ error: jasmine.any(String) })); | ||
| } finally { | ||
| try { | ||
| await fs.unlink(path.join(CANIUSE_DIR, "broken-feature.json")); | ||
| } catch { /* ignore */ } | ||
| } | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| import os from "node:os"; | ||
| import path from "node:path"; | ||
| import { promises as fs } from "node:fs"; | ||
|
|
||
| import { | ||
| getData, | ||
| cache, | ||
| } from "../../../../build/routes/caniuse/lib/index.js"; | ||
|
|
||
| const CANIUSE_DIR = path.join(os.tmpdir(), "caniuse"); | ||
|
|
||
| /** Minimal valid ScraperOutput fixture */ | ||
| const FIXTURE = { | ||
| all: { chrome: [["100", ["y"]]], firefox: [["99", ["n"]]] }, | ||
| summary: { chrome: [["100", ["y"]]] }, | ||
| }; | ||
|
|
||
| async function writeFixture(name, data = FIXTURE) { | ||
| await fs.mkdir(CANIUSE_DIR, { recursive: true }); | ||
| await fs.writeFile( | ||
| path.join(CANIUSE_DIR, `${name}.json`), | ||
| JSON.stringify(data), | ||
| "utf8", | ||
| ); | ||
| } | ||
|
|
||
| async function removeFixture(name) { | ||
| try { | ||
| await fs.unlink(path.join(CANIUSE_DIR, `${name}.json`)); | ||
| } catch { | ||
| // ignore – file may not exist | ||
| } | ||
| } | ||
|
|
||
| describe("caniuse - getData", () => { | ||
| beforeEach(() => cache.clear()); | ||
|
|
||
| it("returns null for empty feature string", async () => { | ||
| expect(await getData("")).toBeNull(); | ||
| }); | ||
|
|
||
| it("returns null for invalid characters (path traversal attempt)", async () => { | ||
| expect(await getData("../etc/passwd")).toBeNull(); | ||
| expect(await getData("foo/bar")).toBeNull(); | ||
| expect(await getData("feature name")).toBeNull(); | ||
| }); | ||
|
|
||
| it("returns null for non-existent feature", async () => { | ||
| expect(await getData("nonexistent-feature-xyz")).toBeNull(); | ||
| }); | ||
|
|
||
| it("returns data for a known feature", async () => { | ||
| await writeFixture("css-grid"); | ||
| try { | ||
| const data = await getData("css-grid"); | ||
| expect(data).toEqual(FIXTURE); | ||
| } finally { | ||
| await removeFixture("css-grid"); | ||
| } | ||
| }); | ||
|
|
||
| it("returns null for wf- edge case (exactly 'wf-')", async () => { | ||
| expect(await getData("wf-")).toBeNull(); | ||
| }); | ||
|
|
||
| it("returns null for wf- feature where stripped name also has no data", async () => { | ||
| expect(await getData("wf-no-such-feature-xyz")).toBeNull(); | ||
| }); | ||
|
|
||
| it("falls back from wf- prefixed key to the stripped feature name", async () => { | ||
| await writeFixture("css-grid"); | ||
| try { | ||
| const data = await getData("wf-css-grid"); | ||
| expect(data).toEqual(FIXTURE); | ||
| } finally { | ||
| await removeFixture("css-grid"); | ||
| } | ||
| }); | ||
|
|
||
| it("caches the result under the original wf- key after a successful fallback", async () => { | ||
| await writeFixture("css-grid"); | ||
| try { | ||
| expect(cache.has("wf-css-grid")).toBeFalse(); | ||
| await getData("wf-css-grid"); | ||
| expect(cache.has("wf-css-grid")).toBeTrue(); | ||
| } finally { | ||
| await removeFixture("css-grid"); | ||
| } | ||
| }); | ||
|
|
||
| it("serves subsequent wf- requests from cache without extra disk I/O", async () => { | ||
| await writeFixture("css-grid"); | ||
| try { | ||
| await getData("wf-css-grid"); // warm up cache | ||
| await removeFixture("css-grid"); // remove file; only cache should serve it now | ||
| const data = await getData("wf-css-grid"); | ||
| expect(data).toEqual(FIXTURE); | ||
| } finally { | ||
| await removeFixture("css-grid"); | ||
| } | ||
| }); | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.