2026-05-29 19:24:46 +08:00
import fs from "node:fs" ;
import path from "node:path" ;
function assert ( condition : unknown , message : string ) {
if ( ! condition ) throw new Error ( message );
}
export function runTests() {
2026-05-30 19:50:50 +08:00
const root = projectRoot ();
2026-05-29 19:24:46 +08:00
const source = fs . readFileSync (
2026-05-30 19:50:50 +08:00
path . join ( root , "components" , "landing" , "InstitutionalLandingPage.tsx" ),
2026-05-29 19:24:46 +08:00
"utf8" ,
);
2026-05-30 23:43:07 +08:00
const authActionsSource = fs . readFileSync (
path . join ( root , "components" , "landing" , "LandingAuthActions.tsx" ),
"utf8" ,
);
const analyticsSource = fs . readFileSync (
path . join ( root , "components" , "landing" , "LandingAnalytics.tsx" ),
"utf8" ,
);
const localeToggleSource = fs . readFileSync (
path . join ( root , "components" , "landing" , "LandingLocaleToggle.tsx" ),
"utf8" ,
);
2026-06-14 05:11:37 +08:00
const localeHelperSource = fs . readFileSync (
path . join ( root , "components" , "landing" , "landingLocale.ts" ),
"utf8" ,
);
2026-06-08 23:43:35 +08:00
const staticCitiesSource = fs . readFileSync (
path . join ( root , "lib" , "static-cities.ts" ),
"utf8" ,
);
2026-05-30 19:50:50 +08:00
const appPageSource = fs . readFileSync ( path . join ( root , "app" , "page.tsx" ), "utf8" );
2026-05-30 23:43:07 +08:00
const publicPngPath = path . join ( root , "public" , "static" , "web.png" );
const fixturePngPath = path . join ( root , "components" , "landing" , "__tests__" , "fixtures" , "web.png" );
2026-05-30 19:50:50 +08:00
const webpPath = path . join ( root , "public" , "static" , "web.webp" );
2026-05-29 19:24:46 +08:00
2026-05-30 23:43:07 +08:00
assert ( ! source . startsWith ( '"use client"' ), "landing body must be a Server Component" );
assert ( ! source . includes ( "@/lib/supabase/client" ), "landing body must not import the Supabase browser client" );
assert ( ! source . includes ( "useEffect" ) && ! source . includes ( "useState" ), "landing body must not hydrate static content with React hooks" );
assert ( ! source . includes ( "lucide-react" ), "landing body must not import lucide-react for the LCP route" );
assert ( authActionsSource . startsWith ( '"use client"' ), "auth actions must be isolated in a client island" );
assert ( analyticsSource . startsWith ( '"use client"' ), "analytics must be isolated in a client island" );
assert ( localeToggleSource . startsWith ( '"use client"' ), "locale toggle must be isolated in a client island" );
assert ( ! authActionsSource . includes ( 'from "@/lib/supabase/client"' ), "auth island must not eagerly import the Supabase browser client" );
assert (
authActionsSource . includes ( "await import(" ) && authActionsSource . includes ( '"@/lib/supabase/client"' ),
"auth island must lazy-load the Supabase browser client after hydration" ,
);
assert ( ! analyticsSource . includes ( 'from "@/lib/app-analytics"' ), "analytics island must lazy-load analytics code" );
assert ( ! authActionsSource . includes ( "lucide-react" ), "auth island must avoid shipping lucide-react" );
assert ( ! localeToggleSource . includes ( "lucide-react" ), "locale island must avoid shipping lucide-react" );
assert ( ! analyticsSource . includes ( "lucide-react" ), "analytics island must avoid shipping lucide-react" );
2026-06-14 05:11:37 +08:00
assert (
localeToggleSource . includes ( "new URL(window.location.href)" ) &&
localeToggleSource . includes ( "LANDING_LOCALE_QUERY_PARAM" ) &&
localeToggleSource . includes ( "window.location.assign(url.toString())" ),
"landing locale toggle must navigate with an explicit locale query so CDN-cached HTML cannot ignore cookie-only changes" ,
);
assert (
localeHelperSource . includes ( "queryLocale" ) &&
source . includes ( "resolveLandingLocale(queryLocale" ) &&
appPageSource . includes ( "<InstitutionalLandingPage queryLocale={landingLocale} />" ),
"landing route must prefer an explicit locale query before cookie or Accept-Language" ,
);
2026-05-29 19:24:46 +08:00
assert ( source . includes ( "3 天免费试用" ), "landing page must advertise the 3-day trial" );
2026-05-29 20:46:35 +08:00
assert ( source . includes ( "试用期权益和 Pro 一致,除了不显示付费 Telegram 群链接" ), "landing page must state trial access matches Pro except the paid group link" );
assert ( ! source . includes ( "高频刷新与 API 仍为 Pro 权益" ), "landing page must not incorrectly exclude high-frequency refresh or API from trial access" );
assert ( source . includes ( "bg-[#fbfbfa]" ), "landing page must use a light Notion-style background" );
assert ( source . includes ( "WeatherWorkflowIllustration" ), "landing page must include a friendly illustration surface" );
2026-06-06 20:43:31 +08:00
assert (
/className="text-base font-black tracking-tight text-slate-950[\s\S]*?>\s*PolyWeather\s*<\/Link>/ . test (
source ,
),
"landing header must expose a readable PolyWeather wordmark instead of a tiny square logo image" ,
);
2026-05-30 23:43:07 +08:00
assert ( ! fs . existsSync ( publicPngPath ), "heavy PNG preview must not remain in public static assets" );
assert ( fs . existsSync ( fixturePngPath ), "PNG preview may only remain as a test fixture" );
2026-05-30 19:50:50 +08:00
assert ( fs . existsSync ( webpPath ), "landing page must ship a WebP preview image for the LCP product screenshot" );
assert (
2026-05-30 23:43:07 +08:00
fs . statSync ( webpPath ). size < fs . statSync ( fixturePngPath ). size * 0.65 ,
2026-05-30 19:50:50 +08:00
"WebP preview must be materially smaller than the PNG LCP image" ,
);
assert ( source . includes ( "/static/web.webp" ), "landing page must load the lighter WebP product preview image" );
2026-06-06 19:17:24 +08:00
assert ( source . includes ( "/static/tel.png" ), "landing page must include the Telegram alert screenshot" );
assert ( source . includes ( "#screenshots" ), "landing navigation must expose the product screenshot section" );
2026-06-08 23:43:35 +08:00
assert ( source . includes ( "#supported-cities" ), "landing navigation must expose the supported cities section" );
assert ( source . includes ( 'id="supported-cities"' ), "landing page must include a supported cities section" );
assert ( source . includes ( "当前支持城市" ), "landing page must tell Chinese users which cities are currently supported" );
assert ( source . includes ( "Supported cities" ), "landing page must tell English users which cities are currently supported" );
assert ( source . includes ( 'from "@/lib/static-cities"' ), "landing supported cities must reuse the static city list instead of hand-writing coverage" );
assert ( source . includes ( "SUPPORTED_CITY_GROUPS" ), "landing supported cities must group coverage by region for scanning" );
assert (
staticCitiesSource . includes ( '"display_name": "Hong Kong"' ) &&
staticCitiesSource . includes ( '"display_name": "New York"' ) &&
staticCitiesSource . includes ( '"display_name": "Shanghai"' ),
"static city fallback must include representative supported markets" ,
);
assert (
source . includes ( "group.cities.map" ) &&
source . includes ( "cityDisplayName(city)" ) &&
source . includes ( "{city.icao}" ),
"landing supported cities must render names and station codes from the generated city groups" ,
);
2026-06-09 13:14:57 +08:00
assert ( source . includes ( "#contact" ), "landing navigation must expose the contact section" );
assert ( source . includes ( 'id="contact"' ), "landing page must include a contact section" );
assert ( source . includes ( "yhrsc30@gmail.com" ), "landing page must show the operator contact email" );
assert ( source . includes ( "mailto:${CONTACT_EMAIL}" ), "landing contact email must be clickable" );
assert ( source . includes ( "https://x.com/polyweatheryuan" ), "landing page must link to the PolyWeather X account" );
2026-06-06 19:17:24 +08:00
assert ( source . includes ( "结算源优先" ) && source . includes ( "差异化卖点" ), "landing page must explain the differentiated settlement-source positioning" );
2026-05-30 19:50:50 +08:00
assert ( ! source . includes ( 'src="/static/web.png"' ), "landing hero must not use the heavy PNG as its primary LCP image" );
assert (
source . includes ( 'width="680"' ) &&
source . includes ( 'height="340"' ) &&
source . includes ( 'fetchPriority="high"' ) &&
source . includes ( 'decoding="async"' ),
"landing product preview must expose stable intrinsic dimensions and high fetch priority" ,
);
assert (
2026-05-30 23:43:07 +08:00
analyticsSource . includes ( '"landing_view"' ) &&
authActionsSource . includes ( '"login_start"' ) &&
authActionsSource . includes ( '"enter_terminal"' ),
"landing client islands must emit the top-of-funnel analytics events" ,
2026-05-30 19:50:50 +08:00
);
2026-05-29 19:24:46 +08:00
assert ( source . includes ( "29.9" ) && source . includes ( "30 天" ), "landing page must show monthly Pro pricing" );
assert ( source . includes ( "79.9" ) && source . includes ( "90 天" ), "landing page must show quarterly Pro pricing" );
2026-06-06 19:17:24 +08:00
assert ( source . includes ( "API" ) && source . includes ( "暂不售卖" ), "landing page must describe API as not currently for sale" );
assert ( ! source . includes ( "Request API" ) && ! source . includes ( "申请 API" ), "landing page must not invite users to buy or request API access" );
assert ( source . includes ( "Team" ) && source . includes ( "团队" ), "landing page must describe the Team tier" );
assert ( source . includes ( "Trial" ) && source . includes ( "Pro" ) && source . includes ( "API" ) && source . includes ( "Team" ), "landing pricing ladder must clearly name Trial / Pro / API / Team" );
2026-05-30 18:04:36 +08:00
assert ( source . includes ( "20 USDC" ) && source . includes ( "+3500 积分" ), "landing page must describe referral discount and reward" );
2026-05-29 20:46:35 +08:00
assert ( ! source . includes ( "AI 气象证据链解读" ), "legacy AI evidence-chain wording must be removed" );
assert ( ! source . includes ( "AI weather evidence" ), "legacy AI evidence wording must be removed" );
2026-05-29 19:24:46 +08:00
assert ( ! source . includes ( "$10" ), "legacy $10/month pricing must be removed from landing page" );
2026-05-29 20:46:35 +08:00
assert ( appPageSource . includes ( 'price: "29.90"' ), "JSON-LD must expose monthly Pro pricing" );
assert ( appPageSource . includes ( 'price: "79.90"' ), "JSON-LD must expose quarterly Pro pricing" );
assert ( ! appPageSource . includes ( 'price: "10.00"' ), "legacy JSON-LD pricing must be removed" );
2026-05-30 23:43:07 +08:00
assert ( ! appPageSource . includes ( "PreloadTerminalData" ), "landing route must not add a fourth client island" );
2026-05-31 01:56:08 +08:00
assert (
! appPageSource . includes ( "AI decision cards" ) &&
! appPageSource . includes ( "AI 气象证据链" ),
"landing metadata and JSON-LD must not advertise the removed AI decision-card positioning" ,
);
2026-05-29 19:24:46 +08:00
}
function projectRoot() {
return process . cwd ();
}