diff --git a/.changeset/lucky-jokes-search.md b/.changeset/lucky-jokes-search.md new file mode 100644 index 000000000..c72ab3ddc --- /dev/null +++ b/.changeset/lucky-jokes-search.md @@ -0,0 +1,13 @@ +--- +"openapi-typescript-helpers": minor +"openapi-typescript": minor +"openapi-fetch": minor +--- + +Add support for the HTTP `QUERY` method ([RFC 10008](https://www.rfc-editor.org/rfc/rfc10008)). + +`query` is recognised as a path item verb in [OpenAPI 3.2](https://spec.openapis.org/oas/v3.2.0.html#path-item-object). openapi-typescript previously dropped it as an unknown property, so no types were emitted for it. + +- `openapi-typescript` now emits a `query` operation for the path items that declare one. Path items without a `query` operation are unchanged, so existing generated output is not affected. +- `openapi-typescript-helpers` adds `"query"` to `HttpMethod`. +- `openapi-fetch` adds `client.QUERY()` (and `QUERY` on the path-based client), which sends a request body like `POST` while preserving QUERY's safe/idempotent semantics. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 014c1f885..7f19b0d58 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,6 +39,9 @@ jobs: - run: pnpm test test-e2e: runs-on: ubuntu-latest + # Browser installation has hung indefinitely before (microsoft/playwright#40724). + # Without a timeout, a hung job burns the full 6h runner limit before being killed. + timeout-minutes: 20 steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 diff --git a/docs/openapi-fetch/api.md b/docs/openapi-fetch/api.md index 9af429290..2e31d5a92 100644 --- a/docs/openapi-fetch/api.md +++ b/docs/openapi-fetch/api.md @@ -43,6 +43,46 @@ client.GET("/my-url", options); | `middleware` | `Middleware[]` | [See docs](/openapi-fetch/middleware-auth) | | (Fetch options) | | Any valid fetch option (`headers`, `mode`, `cache`, `signal`, …) ([docs](https://developer.mozilla.org/en-US/docs/Web/API/fetch#options)) | +## Request methods + +A client exposes one method per HTTP verb: `.GET()`, `.PUT()`, `.POST()`, `.DELETE()`, `.OPTIONS()`, `.HEAD()`, `.PATCH()`, `.TRACE()`, and `.QUERY()`. Each one is typed against the operations your schema declares for that verb, so only the paths that actually support a verb are accepted. + +### QUERY + +[QUERY](https://www.rfc-editor.org/rfc/rfc10008) is a safe, idempotent method that carries a request body — it fills the gap between `GET` (no body) and `POST` (neither safe nor idempotent), and is useful for searches whose parameters are too large or too structured for a URL. + +`query` is recognised as a path item verb in [OpenAPI 3.2](https://spec.openapis.org/oas/v3.2.0.html#path-item-object): + +```yaml +paths: + /resources: + query: + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + ids: + type: array + items: + type: integer + responses: + 200: + description: OK +``` + +```ts +const { data, error } = await client.QUERY("/resources", { + body: { ids: [1, 2, 3] }, +}); +``` + +Because QUERY is safe and idempotent, sending the same request twice must be equivalent to sending it once. openapi-fetch keeps that guarantee: it adds no per-request state of its own, and it does not read or mutate the `body` and `params` you pass in, so the same options object can be reused across retries. + +Note that support is only as good as the runtime and the server. `QUERY` requests are constructed with the standard `Request` API, so any environment that rejects the verb (or any intermediary that doesn't forward it) will fail the request. + ## wrapAsPathBasedClient **wrapAsPathBasedClient** wraps the result of `createClient()` to return a [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy)-based client that allows path-indexed calls: diff --git a/package.json b/package.json index aa1921aba..d7e79f90f 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "@biomejs/biome": "2.4.14", "@changesets/changelog-github": "0.7.0", "@changesets/cli": "2.31.0", - "@playwright/test": "1.59.1", + "@playwright/test": "1.62.1", "@size-limit/preset-small-lib": "12.1.0", "@types/node": "25.6.0", "prettier": "3.8.3", diff --git a/packages/openapi-fetch/src/index.d.ts b/packages/openapi-fetch/src/index.d.ts index 5bca81075..31ccdcff6 100644 --- a/packages/openapi-fetch/src/index.d.ts +++ b/packages/openapi-fetch/src/index.d.ts @@ -231,6 +231,14 @@ export interface Client { request: ClientRequestMethod; /** Call a GET endpoint */ GET: ClientMethod; + /** + * Call a QUERY endpoint + * + * QUERY is safe and idempotent (RFC 10008): unlike POST it may be retried or + * cached, and unlike GET it carries a request body. + * @see https://www.rfc-editor.org/rfc/rfc10008 + */ + QUERY: ClientMethod; /** Call a PUT endpoint */ PUT: ClientMethod; /** Call a POST endpoint */ diff --git a/packages/openapi-fetch/src/index.js b/packages/openapi-fetch/src/index.js index 4c7250fb0..e16b38c51 100644 --- a/packages/openapi-fetch/src/index.js +++ b/packages/openapi-fetch/src/index.js @@ -287,6 +287,13 @@ export default function createClient(clientOptions) { GET(url, init) { return coreFetch(url, { ...init, method: "GET" }); }, + /** + * Call a QUERY endpoint + * @see https://www.rfc-editor.org/rfc/rfc10008 (safe & idempotent; carries a request body) + */ + QUERY(url, init) { + return coreFetch(url, { ...init, method: "QUERY" }); + }, /** Call a PUT endpoint */ PUT(url, init) { return coreFetch(url, { ...init, method: "PUT" }); @@ -348,6 +355,9 @@ class PathCallForwarder { GET = (init) => { return this.client.GET(this.url, init); }; + QUERY = (init) => { + return this.client.QUERY(this.url, init); + }; PUT = (init) => { return this.client.PUT(this.url, init); }; diff --git a/packages/openapi-fetch/test/http-methods/query.test.ts b/packages/openapi-fetch/test/http-methods/query.test.ts new file mode 100644 index 000000000..e32a35b5d --- /dev/null +++ b/packages/openapi-fetch/test/http-methods/query.test.ts @@ -0,0 +1,171 @@ +import { describe, expect, test } from "vitest"; +import { wrapAsPathBasedClient } from "../../src/index.js"; +import { createObservedClient, headersToObj } from "../helpers.js"; +import type { paths } from "./schemas/query.js"; + +describe("QUERY", () => { + test("sends the correct method", async () => { + let method = ""; + const client = createObservedClient({}, async (req) => { + method = req.method; + return Response.json({}); + }); + await client.QUERY("/resources/{id}", { + params: { path: { id: 123 } }, + body: { ids: [1, 2, 3] }, + }); + expect(method).toBe("QUERY"); + }); + + describe("request body", () => { + test("requires necessary requestBodies", async () => { + const client = createObservedClient({}); + + // expect error on missing `body` + await client.QUERY("/resources/{id}", { + params: { path: { id: 1 } }, + // @ts-expect-error + body: undefined, + }); + + // expect error on missing required fields + await client.QUERY("/resources/{id}", { + params: { path: { id: 1 } }, + // @ts-expect-error + body: {}, + }); + + // expect present body to be good enough + await client.QUERY("/resources/{id}", { + params: { path: { id: 1 } }, + body: { ids: [1, 2, 3] }, + }); + }); + + test("requestBody with required: false", async () => { + const client = createObservedClient({}); + + // assert missing `body` doesn't raise a TS error + await client.QUERY("/resources-optional", { + params: { path: { id: 1 } }, + }); + + // assert error on type mismatch + await client.QUERY("/resources-optional", { + params: { path: { id: 1 } }, + body: { + // @ts-expect-error + ids: "not-an-array", + }, + }); + }); + }); + + test("sends correct options, returns success", async () => { + const mockData = { status: "ok" }; + let actualPathname = ""; + const client = createObservedClient({}, async (req) => { + actualPathname = new URL(req.url).pathname; + return Response.json(mockData, { status: 200 }); + }); + + const { data, error, response } = await client.QUERY("/resources/{id}", { + params: { path: { id: 456 } }, + body: { ids: [7, 8, 9] }, + }); + + // assert correct URL was called + expect(actualPathname).toBe("/resources/456"); + + // assert correct data was returned + expect(data).toEqual(mockData); + expect(response.status).toBe(200); + + // assert error is empty + expect(error).toBeUndefined(); + }); + + test("sends the request body with a Content-Type", async () => { + // RFC 10008 §2: a QUERY request has content, so it must identify its media type + let actualBody = ""; + let actualContentType: string | null = ""; + const client = createObservedClient({}, async (req) => { + actualBody = await req.text(); + actualContentType = req.headers.get("Content-Type"); + return Response.json({}); + }); + + await client.QUERY("/resources/{id}", { + params: { path: { id: 123 } }, + body: { ids: [1, 2, 3] }, + }); + + expect(actualBody).toBe(JSON.stringify({ ids: [1, 2, 3] })); + expect(actualContentType).toBe("application/json"); + }); + + // QUERY is defined as safe & idempotent (RFC 10008 §2), so identical calls must stay + // identical on the wire: the client may not add per-request state of its own, and it + // may not consume or mutate the caller’s `init`. + describe("idempotency", () => { + test("repeated identical calls produce identical requests", async () => { + const observed: { method: string; url: string; headers: Record; body: string }[] = []; + const client = createObservedClient({}, async (req) => { + observed.push({ + method: req.method, + url: req.url, + headers: headersToObj(req.headers), + body: await req.text(), + }); + return Response.json({}); + }); + + const init = { + params: { path: { id: 123 } }, + body: { ids: [1, 2, 3] }, + }; + + // reuse the exact same init object, to assert it is not consumed or mutated + await client.QUERY("/resources/{id}", init); + await client.QUERY("/resources/{id}", init); + + expect(observed).toHaveLength(2); + expect(observed[1]).toEqual(observed[0]); + expect(init).toEqual({ params: { path: { id: 123 } }, body: { ids: [1, 2, 3] } }); + }); + + test("is safe: no request body is read or replayed across calls", async () => { + // a request body may only be consumed once, so each call must build its own Request + const bodies: string[] = []; + const client = createObservedClient({}, async (req) => { + bodies.push(await req.text()); + return Response.json({}); + }); + + await client.QUERY("/resources/{id}", { params: { path: { id: 1 } }, body: { ids: [1] } }); + await client.QUERY("/resources/{id}", { params: { path: { id: 1 } }, body: { ids: [1] } }); + + expect(bodies).toEqual([JSON.stringify({ ids: [1] }), JSON.stringify({ ids: [1] })]); + }); + }); + + test("works with the path based client", async () => { + let method = ""; + let actualPathname = ""; + const client = wrapAsPathBasedClient( + createObservedClient({}, async (req) => { + method = req.method; + actualPathname = new URL(req.url).pathname; + return Response.json({}); + }), + ); + + await client["/resources/{id}"].QUERY({ + params: { path: { id: 123 } }, + body: { ids: [1, 2, 3] }, + }); + + expect(method).toBe("QUERY"); + expect(actualPathname).toBe("/resources/123"); + }); +}); diff --git a/packages/openapi-fetch/test/http-methods/schemas/query.d.ts b/packages/openapi-fetch/test/http-methods/schemas/query.d.ts new file mode 100644 index 000000000..d0305b7b4 --- /dev/null +++ b/packages/openapi-fetch/test/http-methods/schemas/query.d.ts @@ -0,0 +1,121 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/resources/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + query: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: components["requestBodies"]["QueryPayload"]; + responses: { + 200: components["responses"]["QueryResult"]; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + error?: string; + }; + }; + }; + }; + }; + }; + "/resources-optional": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + query: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: components["requestBodies"]["QueryPayloadOptional"]; + responses: { + 200: components["responses"]["QueryResult"]; + }; + }; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + QueryPayload: { + ids: number[]; + }; + }; + responses: { + /** @description OK */ + QueryResult: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + status?: string; + data?: Record[]; + }; + }; + }; + }; + parameters: never; + requestBodies: { + QueryPayload: { + content: { + "application/json": components["schemas"]["QueryPayload"]; + }; + }; + QueryPayloadOptional: { + content: { + "application/json": components["schemas"]["QueryPayload"]; + }; + }; + }; + headers: never; + pathItems: never; +} +export type $defs = Record; +export type operations = Record; diff --git a/packages/openapi-fetch/test/http-methods/schemas/query.yaml b/packages/openapi-fetch/test/http-methods/schemas/query.yaml new file mode 100644 index 000000000..86955a490 --- /dev/null +++ b/packages/openapi-fetch/test/http-methods/schemas/query.yaml @@ -0,0 +1,77 @@ +openapi: 3.1.0 +info: + title: openapi-fetch + version: "1.0" +paths: + /resources/{id}: + parameters: + - name: id + in: path + required: true + schema: + type: integer + query: + requestBody: + $ref: "#/components/requestBodies/QueryPayload" + responses: + 200: + $ref: "#/components/responses/QueryResult" + 404: + description: Not Found + content: + application/json: + schema: + type: object + properties: + error: + type: string + /resources-optional: + parameters: + - name: id + in: path + required: true + schema: + type: integer + query: + requestBody: + $ref: "#/components/requestBodies/QueryPayloadOptional" + responses: + 200: + $ref: "#/components/responses/QueryResult" +components: + requestBodies: + QueryPayload: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/QueryPayload" + QueryPayloadOptional: + required: false + content: + application/json: + schema: + $ref: "#/components/schemas/QueryPayload" + responses: + QueryResult: + description: OK + content: + application/json: + schema: + type: object + properties: + status: + type: string + data: + type: array + items: + type: object + schemas: + QueryPayload: + type: object + required: [ids] + properties: + ids: + type: array + items: + type: integer diff --git a/packages/openapi-fetch/test/redocly.yaml b/packages/openapi-fetch/test/redocly.yaml index 1f211f405..b337655d3 100644 --- a/packages/openapi-fetch/test/redocly.yaml +++ b/packages/openapi-fetch/test/redocly.yaml @@ -37,6 +37,10 @@ apis: root: ./http-methods/schemas/trace.yaml x-openapi-ts: output: ./http-methods/schemas/trace.d.ts + http-query: + root: ./http-methods/schemas/query.yaml + x-openapi-ts: + output: ./http-methods/schemas/query.d.ts middleware: root: ./middleware/schemas/middleware.yaml x-openapi-ts: diff --git a/packages/openapi-typescript-helpers/src/index.ts b/packages/openapi-typescript-helpers/src/index.ts index 391e0a694..193a2f59b 100644 --- a/packages/openapi-typescript-helpers/src/index.ts +++ b/packages/openapi-typescript-helpers/src/index.ts @@ -1,6 +1,6 @@ // HTTP types -export type HttpMethod = "get" | "put" | "post" | "delete" | "options" | "head" | "patch" | "trace"; +export type HttpMethod = "get" | "put" | "post" | "delete" | "options" | "head" | "patch" | "trace" | "query"; /** 2XX statuses */ export type OkStatus = 200 | 201 | 202 | 203 | 204 | 206 | 207 | "2XX"; /** 4XX and 5XX statuses */ diff --git a/packages/openapi-typescript/src/transform/path-item-object.ts b/packages/openapi-typescript/src/transform/path-item-object.ts index 87ca02d78..62613e946 100644 --- a/packages/openapi-typescript/src/transform/path-item-object.ts +++ b/packages/openapi-typescript/src/transform/path-item-object.ts @@ -11,7 +11,19 @@ import type { import transformOperationObject, { injectOperationObject } from "./operation-object.js"; import { transformParametersArray } from "./parameters-array.js"; -export type Method = "get" | "put" | "post" | "delete" | "options" | "head" | "patch" | "trace"; +export type Method = "get" | "put" | "post" | "delete" | "options" | "head" | "patch" | "trace" | "query"; + +/** + * Methods emitted on every Path Item Object (absent ones are emitted as `never`). + * + * `query` (RFC 10008) is deliberately left out: it was only introduced in OpenAPI 3.2, + * so it’s emitted for the Path Items that declare it rather than added to every Path + * Item of every document. + */ +const ALWAYS_EMITTED_METHODS = ["get", "put", "post", "delete", "options", "head", "patch", "trace"] as const; + +/** Every method a Path Item Object may declare */ +export const METHODS = [...ALWAYS_EMITTED_METHODS, "query"] as const satisfies readonly Method[]; /** * Transform PathItem nodes (4.8.9) @@ -29,7 +41,9 @@ export default function transformPathItemObject(pathItem: PathItemObject, option ); // methods - for (const method of ["get", "put", "post", "delete", "options", "head", "patch", "trace"] as Method[]) { + const methods: readonly Method[] = "query" in pathItem ? METHODS : ALWAYS_EMITTED_METHODS; + + for (const method of methods) { const operationObject = pathItem[method]; if ( !operationObject || diff --git a/packages/openapi-typescript/src/transform/paths-enum.ts b/packages/openapi-typescript/src/transform/paths-enum.ts index ea2fbdb43..61f349ec6 100644 --- a/packages/openapi-typescript/src/transform/paths-enum.ts +++ b/packages/openapi-typescript/src/transform/paths-enum.ts @@ -2,6 +2,7 @@ import type ts from "typescript"; import { tsEnum } from "../lib/ts.js"; import { getEntries } from "../lib/utils.js"; import type { PathsObject } from "../types.js"; +import { METHODS } from "./path-item-object.js"; export default function makeApiPathsEnum(pathsObject: PathsObject): ts.EnumDeclaration { const enumKeys = []; @@ -9,7 +10,7 @@ export default function makeApiPathsEnum(pathsObject: PathsObject): ts.EnumDecla for (const [url, pathItemObject] of getEntries(pathsObject)) { for (const [method, operation] of Object.entries(pathItemObject)) { - if (!["get", "put", "post", "delete", "options", "head", "patch", "trace"].includes(method)) { + if (!(METHODS as readonly string[]).includes(method)) { continue; } diff --git a/packages/openapi-typescript/src/transform/paths-object.ts b/packages/openapi-typescript/src/transform/paths-object.ts index 83c36af1a..fadc2927e 100644 --- a/packages/openapi-typescript/src/transform/paths-object.ts +++ b/packages/openapi-typescript/src/transform/paths-object.ts @@ -10,7 +10,7 @@ import type { PathsObject, ReferenceObject, } from "../types.js"; -import transformPathItemObject, { type Method } from "./path-item-object.js"; +import transformPathItemObject, { METHODS } from "./path-item-object.js"; const PATH_PARAM_RE = /\{[^}]+\}/g; @@ -114,7 +114,7 @@ function extractPathParams(pathItemObject: PathItemObject, ctx: GlobalContext) { params[resolved.name] = resolved; } } - for (const method of ["get", "put", "post", "delete", "options", "head", "patch", "trace"] as Method[]) { + for (const method of METHODS) { if (!(method in pathItemObject)) { continue; } diff --git a/packages/openapi-typescript/src/types.ts b/packages/openapi-typescript/src/types.ts index d19185cc6..033207b48 100644 --- a/packages/openapi-typescript/src/types.ts +++ b/packages/openapi-typescript/src/types.ts @@ -179,6 +179,8 @@ export interface PathItemObject extends Extensable { patch?: OperationObject | ReferenceObject; /** A definition of a TRACE operation on this path. */ trace?: OperationObject | ReferenceObject; + /** A definition of a QUERY operation on this path (OpenAPI 3.2 / RFC 10008). */ + query?: OperationObject | ReferenceObject; /** An alternative server array to service all operations in this path. */ servers?: ServerObject[]; /** A list of parameters that are applicable for all the operations described under this path. These parameters can be overridden at the operation level, but cannot be removed there. The list MUST NOT include duplicated parameters. A unique parameter is defined by a combination of a name and location. The list can use the Reference Object to link to parameters that are defined at the OpenAPI Object’s components/parameters. */ diff --git a/packages/openapi-typescript/test/transform/path-item-object.test.ts b/packages/openapi-typescript/test/transform/path-item-object.test.ts index 4e0920097..802ce1379 100644 --- a/packages/openapi-typescript/test/transform/path-item-object.test.ts +++ b/packages/openapi-typescript/test/transform/path-item-object.test.ts @@ -416,11 +416,61 @@ describe("transformPathItemObject", () => { head?: never; patch?: never; trace?: never; +}`, + }, + ], + [ + "query > RFC 10008", + { + given: { + query: { + description: "Basic QUERY", + requestBody: { + content: { + "application/json": { $ref: "#/components/schemas/User" }, + }, + }, + responses: { + 200: { $ref: "#/components/responses/AllGood" }, + }, + }, + }, + want: `{ + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + /** @description Basic QUERY */ + query: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["User"]; + }; + }; + responses: { + 200: components["responses"]["AllGood"]; + }; + }; }`, }, ], ]; - for (const [testName, { given, want, options = DEFAULT_OPTIONS, ci }] of tests) { test.skipIf(ci?.skipIf)( testName, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3d3b87bc5..cb3be3a61 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,8 +33,8 @@ importers: specifier: 2.31.0 version: 2.31.0(@types/node@25.6.0) '@playwright/test': - specifier: 1.59.1 - version: 1.59.1 + specifier: 1.62.1 + version: 1.62.1 '@size-limit/preset-small-lib': specifier: 12.1.0 version: 12.1.0(size-limit@12.1.0(jiti@2.6.1)) @@ -116,7 +116,7 @@ importers: dependencies: next: specifier: ^15.5.12 - version: 15.5.12(@playwright/test@1.59.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 15.5.12(@playwright/test@1.62.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) openapi-fetch: specifier: workspace:^ version: link:../.. @@ -1562,9 +1562,9 @@ packages: '@paralleldrive/cuid2@2.3.1': resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} - '@playwright/test@1.59.1': - resolution: {integrity: sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==} - engines: {node: '>=18'} + '@playwright/test@1.62.1': + resolution: {integrity: sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==} + engines: {node: '>=20'} hasBin: true '@polka/url@1.0.0-next.29': @@ -3573,14 +3573,14 @@ packages: pkg-types@2.3.0: resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} - playwright-core@1.59.1: - resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==} - engines: {node: '>=18'} + playwright-core@1.62.1: + resolution: {integrity: sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==} + engines: {node: '>=20'} hasBin: true - playwright@1.59.1: - resolution: {integrity: sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==} - engines: {node: '>=18'} + playwright@1.62.1: + resolution: {integrity: sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==} + engines: {node: '>=20'} hasBin: true pluralize@8.0.0: @@ -5648,9 +5648,9 @@ snapshots: dependencies: '@noble/hashes': 1.8.0 - '@playwright/test@1.59.1': + '@playwright/test@1.62.1': dependencies: - playwright: 1.59.1 + playwright: 1.62.1 '@polka/url@1.0.0-next.29': {} @@ -7585,7 +7585,7 @@ snapshots: neo-async@2.6.2: {} - next@15.5.12(@playwright/test@1.59.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next@15.5.12(@playwright/test@1.62.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@next/env': 15.5.12 '@swc/helpers': 0.5.15 @@ -7603,7 +7603,7 @@ snapshots: '@next/swc-linux-x64-musl': 15.5.12 '@next/swc-win32-arm64-msvc': 15.5.12 '@next/swc-win32-x64-msvc': 15.5.12 - '@playwright/test': 1.59.1 + '@playwright/test': 1.62.1 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' @@ -7757,11 +7757,11 @@ snapshots: exsolve: 1.0.8 pathe: 2.0.3 - playwright-core@1.59.1: {} + playwright-core@1.62.1: {} - playwright@1.59.1: + playwright@1.62.1: dependencies: - playwright-core: 1.59.1 + playwright-core: 1.62.1 optionalDependencies: fsevents: 2.3.2