├── .husky ├── .gitignore └── pre-commit ├── .prettierrc ├── w3c.json ├── generators ├── common.ts ├── respec.d.ts ├── respec.ts └── bikeshed.ts ├── .github ├── workflows │ ├── lint.yml │ └── test.yml └── dependabot.yml ├── tsconfig.json ├── .gitignore ├── package.json ├── LICENSE ├── util.ts ├── test ├── index.test.ts ├── test-util.ts ├── bikeshed.test.ts └── respec.test.ts ├── README.md ├── server.ts └── index.html /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npm run lint -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "auto" 3 | } 4 | -------------------------------------------------------------------------------- /w3c.json: -------------------------------------------------------------------------------- 1 | { 2 | "contacts": ["deniak"], 3 | "repo-type": "tool" 4 | } 5 | -------------------------------------------------------------------------------- /generators/common.ts: -------------------------------------------------------------------------------- 1 | interface SpecGeneratorErrorConstructorOptions { 2 | message: string; 3 | status: number; 4 | } 5 | 6 | export class SpecGeneratorError extends Error { 7 | status: number; 8 | constructor(init: string | SpecGeneratorErrorConstructorOptions) { 9 | const { message, status } = 10 | typeof init === "string" ? { message: init, status: 500 } : init; 11 | super(message); 12 | this.status = status; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Check Lint 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | node-version: [18.x, 20.x] 11 | steps: 12 | - uses: actions/checkout@v6 13 | 14 | - name: Use Node.js ${{ matrix.node-version }} 15 | uses: actions/setup-node@v6.1.0 16 | with: 17 | node-version: ${{ matrix.node-version }} 18 | 19 | - run: npm install 20 | 21 | - run: npm run lint 22 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | time: '00:00' 8 | open-pull-requests-limit: 20 9 | ignore: 10 | - dependency-name: "*" 11 | update-types: ["version-update:semver-patch"] 12 | - dependency-name: "husky" 13 | update-types: ['version-update:semver-minor'] 14 | - package-ecosystem: github-actions 15 | directory: '/' 16 | schedule: 17 | interval: weekly 18 | time: '00:00' 19 | open-pull-requests-limit: 10 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "nodenext", 4 | "moduleResolution": "nodenext", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "noEmit": true, 8 | "allowUnreachableCode": false, 9 | "allowUnusedLabels": false, 10 | "exactOptionalPropertyTypes": true, 11 | "noImplicitOverride": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "skipLibCheck": true, 15 | "strict": true, 16 | "useUnknownInCatchVariables": false, 17 | "verbatimModuleSyntax": true, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Files built from TypeScript 23 | *.js 24 | 25 | # temporary uploaded files 26 | uploads 27 | 28 | # Dependency directory 29 | # Commenting this out is preferred by some people, see 30 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 31 | node_modules 32 | 33 | # Users Environment Variables 34 | .lock-wscript 35 | -------------------------------------------------------------------------------- /generators/respec.d.ts: -------------------------------------------------------------------------------- 1 | interface ToHTMLOptions { 2 | timeout?: number; 3 | disableSandbox?: boolean; 4 | disableGPU?: boolean; 5 | devtools?: boolean; 6 | useLocal?: boolean; 7 | onError?: (error: any) => void; 8 | onProgress?: (msg: string, remaining: number) => void; 9 | onWarning?: (warning: any) => void; 10 | } 11 | 12 | interface ToHTMLMessage { 13 | details?: string; 14 | elements?: any[]; 15 | hint?: string; 16 | message: string; 17 | name: `ReSpec${"Error" | "Warning"}`; 18 | plugin: string; 19 | stack?: string; 20 | title?: string; 21 | } 22 | 23 | declare module "respec" { 24 | export function toHTML( 25 | url: string, 26 | options?: ToHTMLOptions, 27 | ): Promise<{ 28 | html: string; 29 | errors: ToHTMLMessage[]; 30 | warnings: ToHTMLMessage[]; 31 | }>; 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: spec-generator tests 2 | 3 | on: [push, pull_request, workflow_dispatch] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | node-version: [22.x, 24.x] 11 | steps: 12 | - uses: actions/checkout@v6 13 | - name: Use Node.js ${{ matrix.node-version }} 14 | uses: actions/setup-node@v6.1.0 15 | with: 16 | node-version: ${{ matrix.node-version }} 17 | - name: Install bikeshed 18 | run: pipx install bikeshed && bikeshed update 19 | - run: npm install 20 | # See https://chromium.googlesource.com/chromium/src/+/main/docs/security/apparmor-userns-restrictions.md 21 | - name: Disable AppArmor 22 | run: echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns 23 | - run: npx respec2html -e --timeout 30 --src "https://w3c.github.io/spec-generator/respec.html" 24 | - run: npm test 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "w3c-spec-generator", 3 | "version": "3.0.0", 4 | "license": "MIT", 5 | "type": "module", 6 | "dependencies": { 7 | "cheerio": "^1.1.2", 8 | "express": "^5.2.0", 9 | "express-fileupload": "^1.5.0", 10 | "file-type": "^21.1.1", 11 | "filenamify": "^7.0.0", 12 | "respec": "35.6.0", 13 | "tar-stream": "^3.1.1", 14 | "tsx": "^4.21.0", 15 | "typescript": "^5.9.3" 16 | }, 17 | "devDependencies": { 18 | "@types/express": "^5.0.3", 19 | "@types/express-fileupload": "^1.5.1", 20 | "@types/node": "^24.10.0", 21 | "@types/tar-stream": "^3.1.4", 22 | "husky": "^9.0.6", 23 | "prettier": "^3.7.3" 24 | }, 25 | "engines": { 26 | "node": "22 || 24", 27 | "npm": ">=7" 28 | }, 29 | "scripts": { 30 | "build": "tsc --noEmit false", 31 | "lint": "tsc && prettier -c '**/*.ts'", 32 | "fix": "prettier -w '**/*.ts'", 33 | "test": "tsx test/index.test.ts", 34 | "prepare": "husky install", 35 | "start": "tsx server.ts", 36 | "watch": "tsx watch --include index.html server.ts" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 World Wide Web Consortium 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /util.ts: -------------------------------------------------------------------------------- 1 | import type { Request } from "express"; 2 | 3 | /** 4 | * Merges multiple URLSearchParams or FormData instances into the first one passed. 5 | */ 6 | export function mergeParams( 7 | destination: URLSearchParams, 8 | ...sources: URLSearchParams[] 9 | ): URLSearchParams; 10 | export function mergeParams( 11 | destination: FormData, 12 | ...sources: FormData[] | URLSearchParams[] 13 | ): FormData; 14 | export function mergeParams( 15 | destination: URLSearchParams | FormData, 16 | ...sources: URLSearchParams[] | FormData[] 17 | ) { 18 | for (const source of sources) { 19 | for (const [k, v] of source.entries()) { 20 | // Skip file inputs in case of FormData 21 | if (typeof v === "string") destination.set(k, v); 22 | } 23 | } 24 | return destination; 25 | } 26 | 27 | /** 28 | * Merges an Express request's body and query params into one URLSearchParams object. 29 | */ 30 | export function mergeRequestParams(req: Request) { 31 | const queryParams = new URLSearchParams( 32 | req.url.includes("?") ? req.url.slice(req.url.indexOf("?")) : "", 33 | ); 34 | if (!req.body) return queryParams; 35 | return mergeParams(new URLSearchParams(req.body), queryParams); 36 | } 37 | 38 | /** Returns the current (local) date in YYYY-MM-DD format. */ 39 | export const getShortIsoDate = () => new Date().toISOString().slice(0, 10); 40 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import type { Server } from "http"; 2 | import { after, before, describe, it } from "node:test"; 3 | 4 | import { start } from "../server.js"; 5 | import { 6 | createErrorStatusTestCallback, 7 | expectSuccessStatus, 8 | failOnRejection, 9 | TEST_PORT, 10 | testFetchHelpers, 11 | } from "./test-util.js"; 12 | 13 | const { get, post } = testFetchHelpers; 14 | 15 | describe("spec-generator", async () => { 16 | let testServer: Server; 17 | 18 | before(async () => { 19 | testServer = await start(TEST_PORT); 20 | }); 21 | 22 | after(() => testServer.close()); 23 | 24 | describe("General", () => { 25 | describe("fails when it should", () => { 26 | it("without any parameters (GET)", () => 27 | get({}).then( 28 | createErrorStatusTestCallback( 29 | /^{"error":"Both 'type' and 'url' are required"}$/, 30 | ), 31 | failOnRejection, 32 | )); 33 | 34 | it("without any parameters (POST)", () => 35 | post({}).then( 36 | createErrorStatusTestCallback( 37 | /^{"error":"Missing file upload or url"}$/, 38 | ), 39 | failOnRejection, 40 | )); 41 | 42 | it("without type parameter (POST)", () => 43 | post({ url: "https://w3c.github.io/wcag/" }).then( 44 | createErrorStatusTestCallback(/^{"error":"Missing type"}$/), 45 | failOnRejection, 46 | )); 47 | }); 48 | 49 | describe("succeeds when it should", () => { 50 | it("renders form UI upon GET w/ Accept: text/html and no params", () => 51 | get({}, { headers: { Accept: "text/html" } }).then( 52 | expectSuccessStatus, 53 | failOnRejection, 54 | )); 55 | }); 56 | }); 57 | 58 | // Run tests for each generator type, 59 | // within the same top-level suite and server instance 60 | await import("./bikeshed.test.js"); 61 | await import("./respec.test.js"); 62 | }); 63 | -------------------------------------------------------------------------------- /test/test-util.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import { it } from "node:test"; 3 | 4 | import { mergeParams } from "../util.js"; 5 | 6 | export const expectSuccessStatus = async ( 7 | response: Response, 8 | expectedMessage?: RegExp, 9 | ) => { 10 | assert.equal(response.status, 200); 11 | assert.equal(response.statusText, "OK"); 12 | if (expectedMessage) { 13 | const responseText = await response.text(); 14 | assert.match(responseText, expectedMessage); 15 | } 16 | }; 17 | 18 | export const createErrorStatusTestCallback = 19 | (expectedMessage: RegExp, expectedCode = 400) => 20 | async (response: Response) => { 21 | assert.equal(response.status, expectedCode); 22 | const responseText = await response.text(); 23 | assert.match(responseText, expectedMessage); 24 | }; 25 | 26 | export const failOnRejection = (error: Error) => 27 | assert.fail(`Unexpected fetch promise rejection: ${error}`); 28 | 29 | type FetchHelper = ( 30 | params: Record, 31 | init?: RequestInit, 32 | ) => Promise; 33 | 34 | interface FetchHelpers { 35 | get: FetchHelper; 36 | post: FetchHelper; 37 | mixed: FetchHelper; 38 | testAll: ( 39 | message: string, 40 | callback: (request: FetchHelper) => Promise, 41 | ) => void; 42 | } 43 | 44 | export const TEST_PORT = 3000; 45 | const BASE_URL = `http://localhost:${TEST_PORT}/`; 46 | 47 | export const testFetchHelpers: FetchHelpers = { 48 | get(params, init?) { 49 | const url = new URL(BASE_URL); 50 | mergeParams(url.searchParams, new URLSearchParams(params)); 51 | return fetch(url, init); 52 | }, 53 | post(params, init?) { 54 | return fetch(new URL(BASE_URL), { 55 | body: mergeParams(new FormData(), new URLSearchParams(params)), 56 | method: "POST", 57 | ...init, 58 | }); 59 | }, 60 | /** Fetches via POST, but using GET parameters. */ 61 | mixed(params, init?) { 62 | const url = new URL(BASE_URL); 63 | mergeParams(url.searchParams, new URLSearchParams(params)); 64 | return fetch(url, { method: "POST", ...init }); 65 | }, 66 | /** Runs a test across multiple permutations of request methods/parameters. */ 67 | async testAll(message, callback) { 68 | it(`${message} (GET)`, () => callback(testFetchHelpers.get)); 69 | it(`${message} (POST)`, () => callback(testFetchHelpers.post)); 70 | it(`${message} (Mixed)`, () => callback(testFetchHelpers.mixed)); 71 | }, 72 | }; 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Lint status](https://github.com/w3c/spec-generator/actions/workflows/lint.yml/badge.svg)](https://github.com/w3c/spec-generator/actions/workflows/lint.yml) 2 | [![Tests status](https://github.com/w3c/spec-generator/actions/workflows/test.yml/badge.svg)](https://github.com/w3c/spec-generator/actions/workflows/test.yml) 3 | 4 | # Spec Generator 5 | 6 | This exposes a service to automatically generate specs from various source formats. 7 | 8 | ## Setup 9 | 10 | This project requires Node v22 or later. 11 | 12 | Clone or download the repository, then install dependencies: 13 | 14 | ``` 15 | npm install 16 | ``` 17 | 18 | ### Bikeshed preparation 19 | 20 | In order for the server to field requests for Bikeshed documents, 21 | `bikeshed` must be installed such that it is executable by the user running the server. 22 | Version 5.3.6 is required at minimum, as it contains fixes related to JSON output. 23 | 24 | One straightforward installation method is [`pipx`](https://pipx.pypa.io/), 25 | which is designed for installing Python applications (as opposed to libraries), 26 | and is available through various package managers. 27 | 28 | ``` 29 | pipx install bikeshed 30 | ``` 31 | 32 | Note that the latest versions of [Bikeshed now require Python 3.12 or later](https://speced.github.io/bikeshed/#install-py3); 33 | the last versions without this requirement are 5.4.2 or 6.0.0. 34 | 35 | ## Running the server 36 | 37 | Start the server listening on port 8000 by default: 38 | 39 | ```bash 40 | npm start 41 | ``` 42 | 43 | You can specify a port like so: 44 | 45 | ```bash 46 | PORT=3000 npm start 47 | ``` 48 | 49 | When developing, you can use auto-reload: 50 | 51 | ```bash 52 | npm run watch 53 | ``` 54 | 55 | `tsx` can be skipped by building then running directly: 56 | 57 | ```bash 58 | npm run build 59 | node server.js 60 | ``` 61 | 62 | This also supports the `PORT` environment variable as described above. 63 | 64 | To clear out built files, use `git clean`: 65 | 66 | - `git clean -ix` will present an interactive confirmation prompt 67 | - `git clean -fx` will remove the files immediately 68 | 69 | ## API 70 | 71 | Spec Generator has a single endpoint, which is a `GET /`. This endpoint accepts parameters on its 72 | query string. If the call is successful the generated content of the specification is returned. 73 | 74 | * `type` (required). The type of generator for this content. Currently the only supported value is 75 | `respec`. 76 | * `url` (required). The URL of the draft to fetch and generate from. 77 | * `publishDate`. The date at which the publication of this draft is supposed to occur. 78 | 79 | ### Errors 80 | 81 | If a required parameter is missing or has a value that is not understood, the generator returns a 82 | `400` error with a JSON payload the `error` field of which is the human-readable error message. 83 | 84 | If the specific generator encounters a problem a similar error (mostly likely `500`) with the same 85 | sort of JSON message is returned. Specific generator types can extend this behaviour. The `respec` 86 | generator only returns `500` errors. 87 | 88 | The HTTP response status code is `200` even when there are processing errors and warnings. Processing errors and warnings are signaled with the help of `x-errors-count` and `x-warnings-count` response headers respectively instead. 89 | 90 | ## Writing generators 91 | 92 | Generators are simple to write and new ones can easily be added. Simply add a new one under 93 | `generators` and load it into the `genMap` near the top of `server.js`. 94 | 95 | Generators must export a `generate()` method which takes a URL, a set of parameters (from the list 96 | of optional ones that the API supports), and a callback to invoke upon completion. 97 | 98 | If there is an error, the callback's first argument must be an object with a `status` field being 99 | an HTTP error code and a `message` field containing the error message. If the generator is 100 | successful the first argument is `null` and the second is the generated content. 101 | -------------------------------------------------------------------------------- /server.ts: -------------------------------------------------------------------------------- 1 | import type { Server } from "http"; 2 | import { mkdir, readFile, unlink } from "fs/promises"; 3 | import { extname } from "path"; 4 | import { fileURLToPath } from "url"; 5 | 6 | import express, { type Request, type Response } from "express"; 7 | import fileUpload from "express-fileupload"; 8 | 9 | import { generateBikeshed } from "./generators/bikeshed.js"; 10 | import { generateRespec } from "./generators/respec.js"; 11 | import { mergeRequestParams } from "./util.js"; 12 | 13 | const app = express(); 14 | 15 | await mkdir("uploads", { recursive: true }); 16 | app.use( 17 | fileUpload({ 18 | createParentPath: true, 19 | limits: { 20 | fields: 100, 21 | files: 1, 22 | fileSize: 20 * 1048576, 23 | parts: 100, 24 | }, 25 | abortOnLimit: true, 26 | useTempFiles: true, 27 | tempFileDir: "uploads/", 28 | }), 29 | ); 30 | app.use( 31 | "/uploads", 32 | express.static("./uploads", { 33 | setHeaders(res, requestPath) { 34 | const noExtension = !extname(requestPath); 35 | if (noExtension) res.setHeader("Content-Type", "text/html"); 36 | }, 37 | }), 38 | ); 39 | 40 | const FORM_HTML = await readFile("index.html", "utf-8"); 41 | 42 | /** 43 | * Validates HTTP request parameters. 44 | * If validation fails, sends response (error or HTML form) and returns null. 45 | * In other cases, returns an object with information derived from the request, 46 | * and leaves the response to the consuming function. 47 | */ 48 | function validateParams(req: Request, res: Response) { 49 | const params = mergeRequestParams(req); 50 | const type = params.get("type"); 51 | const url = params.get("url"); 52 | const file = req.files?.file; 53 | 54 | if ((!type || !url) && req.method === "GET") { 55 | if (req.headers.accept?.includes("text/html")) res.send(FORM_HTML); 56 | else res.status(400).json({ error: "Both 'type' and 'url' are required" }); 57 | return null; 58 | } 59 | 60 | if (!url && !file) { 61 | res.status(400).json({ error: "Missing file upload or url" }); 62 | return null; 63 | } 64 | 65 | if (!type) { 66 | res.status(400).json({ error: "Missing type" }); 67 | return null; 68 | } 69 | if (!isGeneratorType(type)) { 70 | res.status(400).json({ error: "Invalid type" }); 71 | return null; 72 | } 73 | 74 | if (Array.isArray(file)) { 75 | res.status(400).json({ 76 | error: "Received multiple files; please upload a tar file instead", 77 | }); 78 | return null; 79 | } 80 | 81 | return { file, params, req, res, type, url }; 82 | } 83 | export type ValidateParamsResult = NonNullable< 84 | ReturnType 85 | >; 86 | 87 | const handlers = { 88 | respec: generateRespec, 89 | "bikeshed-spec": generateBikeshed, 90 | "bikeshed-issues-list": generateBikeshed, 91 | }; 92 | type GeneratorType = keyof typeof handlers; 93 | const isGeneratorType = (type: string): type is GeneratorType => 94 | handlers.hasOwnProperty(type); 95 | 96 | app.get("/", async (req, res) => { 97 | const result = validateParams(req, res); 98 | if (!result) return; 99 | await handlers[result.type](result); 100 | }); 101 | 102 | app.post("/", async (req, res) => { 103 | const result = validateParams(req, res); 104 | if (!result) return; 105 | await handlers[result.type](result); 106 | if (result.file) await unlink(result.file.tempFilePath).catch(() => {}); 107 | }); 108 | 109 | /** 110 | * Start listening for HTTP requests. 111 | * @param port - port number to use (optional); defaults to environment variable `$PORT` if exists, and to `8000` if not 112 | */ 113 | export const start = (port = parseInt(process.env.PORT || "", 10) || 8000) => { 114 | console.log(`spec-generator listening on port ${port}`); 115 | const { promise, resolve } = Promise.withResolvers(); 116 | const server = app.listen(port, () => resolve(server)); 117 | return promise; 118 | }; 119 | 120 | if (process.argv[1] === fileURLToPath(import.meta.url) || process.env.pm_id) 121 | await start(); 122 | -------------------------------------------------------------------------------- /test/bikeshed.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from "node:test"; 2 | 3 | import { 4 | createErrorStatusTestCallback, 5 | expectSuccessStatus, 6 | failOnRejection, 7 | testFetchHelpers, 8 | } from "./test-util.js"; 9 | 10 | // Sensor Use Cases at the specified commit emits warning messages, 11 | // and contains non-HTTPS W3C URLs. 12 | const URL_SPEC = 13 | "https://raw.githubusercontent.com/w3c/sensors/d8b0f67c/usecases.bs"; 14 | const URL_SPEC_FATAL = 15 | "https://raw.githubusercontent.com/WICG/background-sync/04f129e5/spec/index.bs"; 16 | const URL_ISSUES_LIST = 17 | "https://raw.githubusercontent.com/w3c/process/562cddb8/issues-20210603.txt"; 18 | 19 | const { get, post, testAll } = testFetchHelpers; 20 | 21 | const failurePattern = /"messageType":"failure"/; 22 | 23 | describe("Bikeshed", () => { 24 | describe("fails when it should", { timeout: 15000 }, () => { 25 | it("without url or file parameter (GET)", () => 26 | get({ type: "bikeshed-spec" }).then( 27 | createErrorStatusTestCallback( 28 | /^{"error":"Both 'type' and 'url' are required"}$/, 29 | ), 30 | failOnRejection, 31 | )); 32 | 33 | it("without url or file parameter (POST)", () => 34 | post({ type: "bikeshed-spec" }).then( 35 | createErrorStatusTestCallback( 36 | /^{"error":"Missing file upload or url"}$/, 37 | ), 38 | failOnRejection, 39 | )); 40 | 41 | testAll("spec mode with a non-spec URL", (request) => 42 | request({ type: "bikeshed-spec", url: URL_ISSUES_LIST }).then( 43 | createErrorStatusTestCallback(failurePattern, 422), 44 | failOnRejection, 45 | ), 46 | ); 47 | 48 | testAll("issues-list mode with a non-issues-list URL", (request) => 49 | request({ type: "bikeshed-issues-list", url: URL_SPEC }).then( 50 | createErrorStatusTestCallback(failurePattern, 422), 51 | failOnRejection, 52 | ), 53 | ); 54 | 55 | testAll( 56 | "when die-on is set and the build produces a message at/above that level", 57 | (request) => 58 | request({ 59 | type: "bikeshed-spec", 60 | url: URL_SPEC, 61 | "die-on": "warning", 62 | }).then( 63 | createErrorStatusTestCallback(failurePattern, 422), 64 | failOnRejection, 65 | ), 66 | ); 67 | }); 68 | 69 | describe("succeeds when it should", { timeout: 40000 }, () => { 70 | it("renders form UI upon GET w/ Accept: text/html and no url", () => 71 | get({ type: "bikeshed-spec" }, { headers: { Accept: "text/html" } }).then( 72 | expectSuccessStatus, 73 | failOnRejection, 74 | )); 75 | 76 | testAll("renders spec, via raw.githubusercontent URL", (request) => 77 | request({ type: "bikeshed-spec", url: URL_SPEC }).then( 78 | expectSuccessStatus, 79 | failOnRejection, 80 | ), 81 | ); 82 | 83 | testAll( 84 | "renders messages instead of spec when output=messages", 85 | (request) => 86 | request({ 87 | type: "bikeshed-spec", 88 | output: "messages", 89 | url: URL_SPEC, 90 | }).then( 91 | (response) => 92 | expectSuccessStatus(response, /"messageType":"success",/), 93 | failOnRejection, 94 | ), 95 | ); 96 | 97 | testAll("renders spec with date overridden", (request) => 98 | request({ 99 | type: "bikeshed-spec", 100 | url: URL_SPEC, 101 | "md-date": "2025-11-10", 102 | }).then( 103 | (response) => 104 | expectSuccessStatus( 105 | response, 106 | /