From 411aa262c7a6e819ca0e77e6e95825c78775013a Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Fri, 25 Sep 2026 10:06:11 -0700 Subject: [PATCH] Test the plugin's copy of the template tag scanner (#711) --- .../lib/resolvedModelName.cases.ts | 79 +++++++++++++++++++ .../yaak-client/lib/resolvedModelName.test.ts | 60 ++------------ .../tests/resolvedModelName.test.ts | 42 ++++++++++ 3 files changed, 127 insertions(+), 54 deletions(-) create mode 100644 apps/yaak-client/lib/resolvedModelName.cases.ts create mode 100644 plugins/template-function-request/tests/resolvedModelName.test.ts diff --git a/apps/yaak-client/lib/resolvedModelName.cases.ts b/apps/yaak-client/lib/resolvedModelName.cases.ts new file mode 100644 index 000000000..230eee430 --- /dev/null +++ b/apps/yaak-client/lib/resolvedModelName.cases.ts @@ -0,0 +1,79 @@ +/* oxlint-disable no-template-curly-in-string */ + +/* + * Shared expectations for `resolvedModelName`. + * + * The request template function plugin carries a hand-copied `resolvedModelName` and template-tag + * scanner, because importing this module into the plugin bundle fails to build on Windows in CI. + * Both `resolvedModelName.test.ts` here and `plugins/template-function-request/tests/ + * resolvedModelName.test.ts` iterate these cases, so changing an expectation on this side + * automatically exercises the plugin's copy and a drifting copy fails its own suite. + * + * Keep this file dependency-free — the plugin test imports it by relative path, so anything it + * pulls in would have to resolve from both packages. + */ + +export interface ResolvedModelNameCase { + /** Test name, used as the `test.each` title on both sides. */ + name: string; + /** `url` of the `http_request` under test. */ + url: string; + /** `name` of the `http_request`, when the case is about an explicit name. */ + requestName?: string; + /** What `resolvedModelName` must return. */ + expected: string; +} + +export const resolvedModelNameCases: ResolvedModelNameCase[] = [ + { + name: "returns the name when there is one", + url: "https://example.com", + requestName: "My Request", + expected: "My Request", + }, + { + name: "replaces a simple tag with its contents", + url: "${[ base_url ]}/users", + expected: "base_url/users", + }, + { + name: "replaces a tag containing a quoted argument with a space", + url: "${[ fn(arg='my key') ]}/users", + expected: "fn(arg='my key')/users", + }, + { + name: "replaces each tag in a multi-tag string", + url: "${[ fn(arg='my key') ]}/a/${[ other(b='x y') ]}/b", + expected: "fn(arg='my key')/a/other(b='x y')/b", + }, + { + name: "keeps a quoted `]}` inside the tag", + url: "${[ fn(arg='x]}y') ]}/users", + expected: "fn(arg='x]}y')/users", + }, + { + name: "keeps an escaped quote inside the tag", + url: "${[ fn(arg='it\\'s ]}') ]}/users", + expected: "fn(arg='it\\'s ]}')/users", + }, + { + name: "closes at the first `]}` when a quote is left unterminated", + url: "${[ fn(arg='oops ]}/users", + expected: "fn(arg='oops/users", + }, + { + name: "stops each tag at its first closing bracket", + url: "${[ a ]}${[ b ]}", + expected: "ab", + }, + { + name: "strips the protocol", + url: "https://example.com/${[ path ]}", + expected: "example.com/path", + }, + { + name: "falls back to a generic name when the url is empty", + url: "", + expected: "HTTP Request", + }, +]; diff --git a/apps/yaak-client/lib/resolvedModelName.test.ts b/apps/yaak-client/lib/resolvedModelName.test.ts index 55acf6474..b945c1b21 100644 --- a/apps/yaak-client/lib/resolvedModelName.test.ts +++ b/apps/yaak-client/lib/resolvedModelName.test.ts @@ -1,5 +1,6 @@ import type { HttpRequest } from "@yaakapp-internal/models"; import { describe, expect, test, vi } from "vite-plus/test"; +import { resolvedModelNameCases } from "./resolvedModelName.cases"; // resolvedModelName only reads plain model fields, but its module imports pull in the // platform singleton, which needs a browser. Stub them out to keep this a pure unit test. @@ -8,62 +9,13 @@ vi.mock("./jotai", () => ({ jotaiStore: { get: () => [] } })); const { resolvedModelName } = await import("./resolvedModelName"); -function httpRequest(url: string): HttpRequest { - return { id: "rq_test", model: "http_request", name: "", url } as HttpRequest; +function httpRequest(url: string, name = ""): HttpRequest { + return { id: "rq_test", model: "http_request", name, url } as HttpRequest; } describe("resolvedModelName", () => { - test("returns the name when there is one", () => { - expect( - resolvedModelName({ ...httpRequest("https://example.com"), name: "My Request" }), - ).toEqual("My Request"); - }); - - test("replaces a simple tag with its contents", () => { - expect(resolvedModelName(httpRequest("${[ base_url ]}/users"))).toEqual("base_url/users"); - }); - - test("replaces a tag containing a quoted argument with a space", () => { - expect(resolvedModelName(httpRequest("${[ fn(arg='my key') ]}/users"))).toEqual( - "fn(arg='my key')/users", - ); - }); - - test("replaces each tag in a multi-tag string", () => { - expect( - resolvedModelName(httpRequest("${[ fn(arg='my key') ]}/a/${[ other(b='x y') ]}/b")), - ).toEqual("fn(arg='my key')/a/other(b='x y')/b"); - }); - - test("keeps a quoted `]}` inside the tag", () => { - expect(resolvedModelName(httpRequest("${[ fn(arg='x]}y') ]}/users"))).toEqual( - "fn(arg='x]}y')/users", - ); - }); - - test("keeps an escaped quote inside the tag", () => { - expect(resolvedModelName(httpRequest("${[ fn(arg='it\\'s ]}') ]}/users"))).toEqual( - "fn(arg='it\\'s ]}')/users", - ); - }); - - test("closes at the first `]}` when a quote is left unterminated", () => { - expect(resolvedModelName(httpRequest("${[ fn(arg='oops ]}/users"))).toEqual( - "fn(arg='oops/users", - ); - }); - - test("stops each tag at its first closing bracket", () => { - expect(resolvedModelName(httpRequest("${[ a ]}${[ b ]}"))).toEqual("ab"); - }); - - test("strips the protocol", () => { - expect(resolvedModelName(httpRequest("https://example.com/${[ path ]}"))).toEqual( - "example.com/path", - ); - }); - - test("falls back to a generic name when the url is empty", () => { - expect(resolvedModelName(httpRequest(""))).toEqual("HTTP Request"); + // Shared with the plugin's hand-copied scanner. See `resolvedModelName.cases.ts`. + test.each(resolvedModelNameCases)("$name", ({ url, requestName, expected }) => { + expect(resolvedModelName(httpRequest(url, requestName))).toEqual(expected); }); }); diff --git a/plugins/template-function-request/tests/resolvedModelName.test.ts b/plugins/template-function-request/tests/resolvedModelName.test.ts new file mode 100644 index 000000000..5a04495e4 --- /dev/null +++ b/plugins/template-function-request/tests/resolvedModelName.test.ts @@ -0,0 +1,42 @@ +/* + * This plugin carries its own copy of the template-tag scanner, because importing the client's + * copy into the plugin bundle fails to build on Windows in CI. + * + * Rather than copying the expectations too — copied expectations can't detect drift — this + * imports the client's `resolvedModelName.cases.ts` and runs the plugin's copy against it, then + * asserts the plugin's output matches the client's own `resolvedModelName` for every case. A + * change to the client scanner or its cases therefore fails here until the copy is updated. The + * import is test-only and never reaches the plugin bundle, so the Windows problem doesn't apply. + */ + +import type { HttpRequest } from "@yaakapp-internal/models"; +import { describe, expect, test, vi } from "vite-plus/test"; +import { resolvedModelNameCases } from "../../../apps/yaak-client/lib/resolvedModelName.cases"; + +// `resolvedModelName` reads plain model fields, but the two modules under test pull in sibling +// plugins that only resolve once they're built, plus the client's platform singleton, which +// needs a browser. Stub them all out to keep this a pure unit test. +vi.mock("../../template-function-json", () => ({ filterJSONPath: () => null })); +vi.mock("../../template-function-xml", () => ({ filterXPath: () => null })); +vi.mock("@yaakapp-internal/models", () => ({ foldersAtom: {} })); +vi.mock("../../../apps/yaak-client/lib/jotai", () => ({ jotaiStore: { get: () => [] } })); + +const { resolvedModelName } = await import("../src"); +const { resolvedModelName: clientResolvedModelName } = await import( + "../../../apps/yaak-client/lib/resolvedModelName" +); + +function httpRequest(url: string, name = ""): HttpRequest { + return { id: "rq_test", model: "http_request", name, url } as HttpRequest; +} + +describe("resolvedModelName", () => { + test.each(resolvedModelNameCases)("$name", ({ url, requestName, expected }) => { + expect(resolvedModelName(httpRequest(url, requestName))).toEqual(expected); + }); + + test.each(resolvedModelNameCases)("matches the client for: $name", ({ url, requestName }) => { + const request = httpRequest(url, requestName); + expect(resolvedModelName(request)).toEqual(clientResolvedModelName(request)); + }); +});