diff --git a/e2e/fixtures/keyword-research-fixtures.ts b/e2e/fixtures/keyword-research-fixtures.ts index fa8bf840..1a5c181e 100644 --- a/e2e/fixtures/keyword-research-fixtures.ts +++ b/e2e/fixtures/keyword-research-fixtures.ts @@ -1,4 +1,4 @@ -import type { KeywordResearchRow } from "@/types/keywords"; +import type { KeywordResearchRow, SerpResultItem } from "@/types/keywords"; import type { ResolvedResearchKeywordsInput } from "@/types/schemas/keywords"; const MONTHLY_SEARCHES = [ @@ -71,3 +71,30 @@ export function getKeywordResearchFixture(data: ResolvedResearchKeywordsInput) { }, }; } + +function makeSerpItem(keyword: string, index: number): SerpResultItem { + const rank = index + 1; + return { + rank, + title: `${keyword} - result ${rank}`, + url: `https://example-${rank}.com/${keyword.replace(/\s+/g, "-")}`, + domain: `example-${rank}.com`, + description: `Fixture SERP description for ${keyword}, position ${rank}.`, + etv: 5000 - index * 400, + estimatedPaidTrafficCost: Number((1200 - index * 90).toFixed(2)), + referringDomains: 900 - index * 60, + backlinks: 12_000 - index * 800, + isNew: false, + rankChange: null, + }; +} + +export function getSerpAnalysisFixture(data: { keyword: string }) { + const keyword = data.keyword.trim().toLowerCase(); + return { + requestedKeyword: keyword, + items: Array.from({ length: 10 }, (_, index) => + makeSerpItem(keyword, index), + ), + }; +} diff --git a/src/serverFunctions/config.ts b/src/serverFunctions/config.ts index a78a1886..21a2f110 100644 --- a/src/serverFunctions/config.ts +++ b/src/serverFunctions/config.ts @@ -2,9 +2,19 @@ import { env } from "cloudflare:workers"; import { createServerFn } from "@tanstack/react-start"; import { requireAuthenticatedContext } from "@/serverFunctions/middleware"; +// E2E runs serve fixture data and never call DataForSEO, so a missing key must +// not raise the blocking setup modal — it covers the page and eats every click. +function shouldUseE2eFixtures() { + return ( + import.meta.env.VITE_E2E_KEYWORD_FIXTURES === "1" || + import.meta.env.VITE_E2E_DOMAIN_FIXTURES === "1" + ); +} + export const getSeoApiKeyStatus = createServerFn({ method: "GET" }) .middleware(requireAuthenticatedContext) .handler(() => { - const configured = Boolean(env.DATAFORSEO_API_KEY?.trim()); + const configured = + shouldUseE2eFixtures() || Boolean(env.DATAFORSEO_API_KEY?.trim()); return { configured }; }); diff --git a/src/serverFunctions/keywords.ts b/src/serverFunctions/keywords.ts index 293cbd43..6e4c211f 100644 --- a/src/serverFunctions/keywords.ts +++ b/src/serverFunctions/keywords.ts @@ -123,13 +123,16 @@ export const refreshSavedKeywordMetrics = createServerFn({ method: "POST" }) export const getSerpAnalysis = createServerFn({ method: "POST" }) .middleware(requireProjectContext) .validator(serpAnalysisSchema) - .handler(async ({ data, context }) => - KeywordResearchService.getSerpAnalysis( - { - ...data, - ...resolveMarket(data, context.project), - projectId: context.projectId, - }, - context, - ), - ); + .handler(async ({ data, context }) => { + const input = { + ...data, + ...resolveMarket(data, context.project), + projectId: context.projectId, + }; + if (shouldUseKeywordE2eFixtures()) { + const fixtures = await getKeywordE2eFixtures(); + return fixtures.getSerpAnalysisFixture(input); + } + + return KeywordResearchService.getSerpAnalysis(input, context); + });