| ... | @@ -160,6 +160,109 @@ it.layer(TestLayer)("ProjectFaviconResolverLive", (it) => { | ... | @@ -160,6 +160,109 @@ it.layer(TestLayer)("ProjectFaviconResolverLive", (it) => { |
| 160 | }), | 160 | }), |
| 161 | ); | 161 | ); |
| 162 | | 162 | |
| | 163 | it.effect("resolves icon hrefs from object-literal route metadata", () => |
| | 164 | Effect.gen(function* () { |
| | 165 | const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver; |
| | 166 | const cwd = yield* makeTempDir; |
| | 167 | yield* writeTextFile( |
| | 168 | cwd, |
| | 169 | "src/routes/__root.tsx", |
| | 170 | `export const Route = createRootRoute({ |
| | 171 | head: () => ({ |
| | 172 | links: [ |
| | 173 | { rel: "stylesheet", href: "/app.css" }, |
| | 174 | { rel: "icon", href: "/brand/logo.svg" }, |
| | 175 | ], |
| | 176 | }), |
| | 177 | });`, |
| | 178 | ); |
| | 179 | yield* writeTextFile(cwd, "public/brand/logo.svg", "<svg>brand</svg>"); |
| | 180 | |
| | 181 | const resolved = yield* resolver.resolvePath(cwd); |
| | 182 | |
| | 183 | expect(resolved).not.toBeNull(); |
| | 184 | expect(resolved).toContain("public/brand/logo.svg"); |
| | 185 | }), |
| | 186 | ); |
| | 187 | |
| | 188 | it.effect("resolves object-literal icon metadata when href precedes rel", () => |
| | 189 | Effect.gen(function* () { |
| | 190 | const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver; |
| | 191 | const cwd = yield* makeTempDir; |
| | 192 | yield* writeTextFile( |
| | 193 | cwd, |
| | 194 | "src/root.tsx", |
| | 195 | `const links = [{ href: "/brand/logo.svg", rel: "shortcut icon" }];`, |
| | 196 | ); |
| | 197 | yield* writeTextFile(cwd, "public/brand/logo.svg", "<svg>brand</svg>"); |
| | 198 | |
| | 199 | const resolved = yield* resolver.resolvePath(cwd); |
| | 200 | |
| | 201 | expect(resolved).not.toBeNull(); |
| | 202 | expect(resolved).toContain("public/brand/logo.svg"); |
| | 203 | }), |
| | 204 | ); |
| | 205 | |
| | 206 | it.effect("resolves object-literal icon metadata alongside nested objects", () => |
| | 207 | Effect.gen(function* () { |
| | 208 | const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver; |
| | 209 | const cwd = yield* makeTempDir; |
| | 210 | yield* writeTextFile( |
| | 211 | cwd, |
| | 212 | "src/root.tsx", |
| | 213 | `const links = [{ attributes: {}, rel: "icon", href: "/brand/logo.svg" }];`, |
| | 214 | ); |
| | 215 | yield* writeTextFile(cwd, "public/brand/logo.svg", "<svg>brand</svg>"); |
| | 216 | |
| | 217 | const resolved = yield* resolver.resolvePath(cwd); |
| | 218 | |
| | 219 | expect(resolved).not.toBeNull(); |
| | 220 | expect(resolved).toContain("public/brand/logo.svg"); |
| | 221 | }), |
| | 222 | ); |
| | 223 | |
| | 224 | it.effect("skips icon metadata without an href and keeps scanning", () => |
| | 225 | Effect.gen(function* () { |
| | 226 | const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver; |
| | 227 | const cwd = yield* makeTempDir; |
| | 228 | yield* writeTextFile( |
| | 229 | cwd, |
| | 230 | "src/root.tsx", |
| | 231 | `const links = [{ rel: "icon" }, { rel: "icon", href: "/brand/logo.svg" }];`, |
| | 232 | ); |
| | 233 | yield* writeTextFile(cwd, "public/brand/logo.svg", "<svg>brand</svg>"); |
| | 234 | |
| | 235 | const resolved = yield* resolver.resolvePath(cwd); |
| | 236 | |
| | 237 | expect(resolved).not.toBeNull(); |
| | 238 | expect(resolved).toContain("public/brand/logo.svg"); |
| | 239 | }), |
| | 240 | ); |
| | 241 | |
| | 242 | // A large icon source with no icon metadata used to pin the server's event loop for |
| | 243 | // minutes: the object pattern was unanchored, so it restarted at every offset and |
| | 244 | // rescanned forward from each one. Anchoring keeps this proportional to file size. |
| | 245 | it.effect("scans large icon sources without an icon in reasonable time", () => |
| | 246 | Effect.gen(function* () { |
| | 247 | const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver; |
| | 248 | const cwd = yield* makeTempDir; |
| | 249 | // Mirrors a generated single-file build: large, brace-sparse, and no icon metadata. |
| | 250 | const filler = `<p>${"pokopia companion guide ".repeat(24)}</p>\n`; |
| | 251 | yield* writeTextFile( |
| | 252 | cwd, |
| | 253 | "index.html", |
| | 254 | `<!doctype html><html><head><title>guide</title></head><body>\n${filler.repeat(1200)}</body></html>`, |
| | 255 | ); |
| | 256 | |
| | 257 | const startedAt = performance.now(); |
| | 258 | const resolved = yield* resolver.resolvePath(cwd); |
| | 259 | const elapsedMs = performance.now() - startedAt; |
| | 260 | |
| | 261 | expect(resolved).toBeNull(); |
| | 262 | expect(elapsedMs).toBeLessThan(5_000); |
| | 263 | }), |
| | 264 | ); |
| | 265 | |
| 163 | it.effect("returns null when no icon is present", () => | 266 | it.effect("returns null when no icon is present", () => |
| 164 | Effect.gen(function* () { | 267 | Effect.gen(function* () { |
| 165 | const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver; | 268 | const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver; |