fix: local dev proxy, TypeScript errors, and test quality
All checks were successful
build-and-release / build (push) Successful in 2m15s

- middleware.ts: default GATEWAY_URL to localhost:9100 (was K8s hostname);
  proxy all /api/* to gateway instead of falling through to SolidStart renderer
- signup/index.tsx: fix kebab-case style property (align-items)
- dashboard/CreditsPage.tsx: fix IIFE closing in Show children
- AskAsh/index.ts: use ~ alias to fix bundler module resolution
- vite.config.ts: add string type to rewrite callback
- global.d.ts: add Window.__captchaCode and __testMode declarations
- help-center/article/[slug].tsx: add missing ContentBlock import
- test/setup.ts: add vitest/globals reference, fix IntersectionObserver mock
- tests/e2e: fix implicit any, string|null/undefined type errors in e2e specs
- tests/tsconfig.json: add tests-specific tsconfig with node types

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tracewebstudio Dev 2026-07-17 00:47:32 +02:00
parent 0b38bc6ca5
commit 4efe848f3a
30 changed files with 76 additions and 62 deletions

View file

@ -12,7 +12,7 @@ export {
generateContent,
purchaseCredits,
CREDIT_COSTS,
} from '../hooks/useAiCredits';
} from '~/hooks/useAiCredits';
// Types
export type {
@ -22,4 +22,4 @@ export type {
PurchaseRequest,
PurchaseResponse,
ApiError,
} from '../hooks/useAiCredits';
} from '~/hooks/useAiCredits';

View file

@ -65,7 +65,7 @@ export default function DashboardLayout(props: ParentProps) {
return;
}
const storageKeys = [
const storageKeys: [string, Storage][] = [
["nxtgauge_signup_profile_v1", localStorage],
["nxtgauge_auth_user", localStorage],
["nxtgauge_user", localStorage],

View file

@ -477,7 +477,7 @@ export default function AiCreditsAdmin() {
setLedgerPage(p => p + 1);
handleFetchLedger();
}}
disabled={ledgerData()?.data.length === 0 || ledgerData()?.data.length < 20}
disabled={ledgerData()?.data.length === 0 || (ledgerData()?.data.length ?? 0) < 20}
style={{
padding: '8px 16px',
border: '1px solid #D1D5DB',

View file

@ -5599,7 +5599,7 @@ export default function DashboardDesignPreview(props: {
<div style="display:flex;justify-content:space-between;margin-top:12px;padding-top:12px;border-top:2px solid #F3F4F6">
<span style="font-size:15px;font-weight:800;color:#111827">Total Amount</span>
<span style="font-size:18px;font-weight:800;color:#FF5E13">{((appliedCoupon() ? appliedCoupon().final_price_inr : Number(pkg.price_paise)) / 100).toLocaleString('en-IN')}</span>
<span style="font-size:18px;font-weight:800;color:#FF5E13">{((appliedCoupon()?.final_price_inr ?? Number(pkg.price_paise)) / 100).toLocaleString('en-IN')}</span>
</div>
</div>
</div>

View file

@ -1272,7 +1272,7 @@ export default function CreditsPage(props: Props) {
"justify-content": "center",
}}
>
<span style={{ fontSize: "18px" }}>🪙</span>
<span style={{ "font-size": "18px" }}>🪙</span>
</div>
<div>
<p
@ -1342,7 +1342,7 @@ export default function CreditsPage(props: Props) {
</For>
</div>
);
})}
})()}
</Show>
</div>
</Show>
@ -1727,7 +1727,7 @@ export default function CreditsPage(props: Props) {
{formatDate(item.created_at)}
</p>
</div>
<div style={{ textAlign: "right" }}>
<div style={{ "text-align": "right" }}>
<p
style={{
margin: "0",
@ -1776,7 +1776,7 @@ export default function CreditsPage(props: Props) {
{formatDate(payment.created_at)}
</p>
</div>
<div style={{ textAlign: "right" }}>
<div style={{ "text-align": "right" }}>
<p
style={{
margin: "0",

View file

@ -132,7 +132,7 @@ export default function ExploreServicesPage() {
const data = await res.json().catch(() => ({}));
if (res.ok) {
const roles: string[] = Array.isArray(data) ? data : Array.isArray(data?.data) ? data.data : [];
setActiveRoles(roles.map((r) => String(typeof r === "string" ? r : r.role_key || r.key || "").toUpperCase()));
setActiveRoles(roles.map((r) => String(typeof r === "string" ? r : (r as any).role_key || (r as any).key || "").toUpperCase()));
} else {
setActiveRoles([]);
}
@ -496,13 +496,11 @@ export default function ExploreServicesPage() {
"font-size": "12px",
"font-weight": "700",
cursor:
busyRoleKey() === card.key ||
card.action === "Current Role"
busyRoleKey() === card.key
? "not-allowed"
: "pointer",
opacity:
busyRoleKey() === card.key ||
card.action === "Current Role"
busyRoleKey() === card.key
? "0.6"
: "1",
"margin-top": "auto",
@ -517,13 +515,11 @@ export default function ExploreServicesPage() {
"font-size": "12px",
"font-weight": "700",
cursor:
busyRoleKey() === card.key ||
card.action === "Current Role"
busyRoleKey() === card.key
? "not-allowed"
: "pointer",
opacity:
busyRoleKey() === card.key ||
card.action === "Current Role"
busyRoleKey() === card.key
? "0.6"
: "1",
"margin-top": "auto",

View file

@ -416,7 +416,6 @@ export default function MyDashboardPage(props: Props) {
if (customizeMode() && !visibleWidgets().has(key)) return null;
return (
<div
key={key}
data-widget-key={key}
draggable={true}
onDragStart={(e) => {

View file

@ -726,7 +726,7 @@ export default function PortfolioPage(props: Props) {
};
const tabs = runtimePortfolioTabs();
const fieldsByTab = runtimeFieldsByTab();
const activeTab = () => tabs.includes(jobSeekerTab()) ? jobSeekerTab() : tabs[0];
const activeTab = () => (tabs as string[]).includes(jobSeekerTab()) ? jobSeekerTab() : tabs[0];
const activeFields = () => fieldsByTab[activeTab()] || [];
const isLongField = (field: string) => {
const key = normalizeToken(field);

View file

@ -64,7 +64,7 @@ export default function PortfolioWidget(props: Props) {
<For each={[
['Portfolio Items', total(), '#374151'],
]}>
{([label, value, color]) => <PortfolioStatRow label={label} value={value} color={color} />}
{([label, value, color]) => <PortfolioStatRow label={label as string} value={String(value)} color={color as string} />}
</For>
</Show>
</DashboardWidget>

7
src/global.d.ts vendored
View file

@ -1,4 +1,11 @@
/// <reference types="vinxi/types/client" />
/// <reference types="vinxi/types/server" />
declare global {
interface Window {
__captchaCode?: string;
__testMode?: boolean;
}
}
export {};

View file

@ -1,4 +1,4 @@
import { createSignal, createResource, createEffect, Accessor } from 'solid-js';
import { createSignal, createResource, createEffect, Accessor, Resource } from 'solid-js';
// API base URL - can be configured via environment
const API_BASE = import.meta.env.VITE_API_BASE_URL || '';
@ -185,7 +185,7 @@ export interface UseAiCreditsReturn {
// Credit data
credits: Accessor<number>;
tracecoinBalance: Accessor<number>;
creditResource: ReturnType<typeof createResource<CreditBalance>>;
creditResource: Resource<CreditBalance>;
// Loading states
isLoading: Accessor<boolean>;

View file

@ -23,7 +23,7 @@
import { createMiddleware } from "@solidjs/start/middleware";
const GATEWAY_URL = (
process.env.GATEWAY_URL || "http://nxtgauge-rust-gateway:9100"
process.env.GATEWAY_URL || "http://localhost:9100"
).replace(/\/+$/, "");
const PUBLIC_API_URL = (
@ -202,8 +202,7 @@ export default createMiddleware({
return handlePayuReturn(fetchEvent);
}
// Everything else under /api/* — let it fall through.
// Returning undefined tells the framework to continue.
return;
// All other /api/* paths — proxy to the gateway
return proxyToGateway(fetchEvent, path);
},
});

View file

@ -386,7 +386,7 @@ export default function AboutPage() {
? `perspective(980px) rotateX(${builtTilt().x}deg) rotateY(${builtTilt().y}deg) scale(1)`
: 'perspective(980px) rotateX(0deg) rotateY(0deg) scale(0.95)',
filter: builtVisible() && !reduceMotion() ? 'blur(0px)' : 'blur(3px)',
transitionDelay: builtVisible() ? '150ms' : '0ms',
"transition-delay": builtVisible() ? '150ms' : '0ms',
}}
>
<span class="about-chapter-two-panel-glow" />

View file

@ -267,7 +267,7 @@ export default function ContactPage() {
</span>
<span class="contact-upload-text">
{values().attachment
? values().attachment.name
? values().attachment?.name
: "Upload pdf/png/jpg (max 10MB)"}
</span>
<input

View file

@ -39,6 +39,7 @@ interface Invoice {
issued_at: string;
paid_at: string | null;
voided_at: string | null;
void_reason: string | null;
}
interface LineItem {
@ -118,7 +119,7 @@ export default function InvoiceDetailPage() {
</div>
</Show>
<Show when={data()}>
<Show when={data()} keyed>
{(d: InvoiceResponse) => (
<div class="bg-white border rounded-xl overflow-hidden shadow-sm">
<div class="bg-gray-900 text-white p-6">

View file

@ -61,7 +61,7 @@ export default function InvoicesPage() {
</div>
</Show>
<Show when={data()}>
<Show when={data()} keyed>
{(d) => (
<Show
when={d.invoices.length > 0}

View file

@ -6,6 +6,7 @@ import PublicBackground from "~/components/PublicBackground";
import PublicHeader from "~/components/PublicHeader";
import PublicFooter from "~/components/PublicFooter";
import ArticleContent from "~/components/ArticleContent";
import type { ContentBlock } from "~/data/help-center-seed";
function categoryTitle(input: string) {
return input
@ -326,7 +327,7 @@ export default function HelpCenterArticlePage() {
when={a().content}
fallback={<p style={{ color: "#6B7280" }}>No content available.</p>}
>
<ArticleContent blocks={a().content} />
<ArticleContent blocks={a().content as ContentBlock[]} />
</Show>
<div
style={{

View file

@ -146,7 +146,7 @@ export default function HelpCenterPage() {
<div class="container" style={{ "max-width": "1100px" }}>
{/* Categories - Glass Cards */}
<Show when={!query() && !category()}>
<div style={{ marginBottom: "60px" }}>
<div style={{ "margin-bottom": "60px" }}>
<h2
style={{
margin: "0 0 28px",
@ -237,7 +237,7 @@ export default function HelpCenterPage() {
</Show>
{/* Articles - Glass Cards */}
<div style={{ marginTop: !query() && !category() ? "0" : "20px" }}>
<div style={{ "margin-top": !query() && !category() ? "0" : "20px" }}>
<h2
style={{
margin: "0 0 28px",

View file

@ -16,7 +16,7 @@ export default function SignupIndexRoute() {
const [search] = useSearchParams();
createEffect(() => {
const intent = search.intent?.toLowerCase() || "";
const intent = (typeof search.intent === 'string' ? search.intent : Array.isArray(search.intent) ? search.intent[0] : '')?.toLowerCase() || "";
if (intent.includes("company")) {
navigate("/signup/company", { replace: true });
@ -35,8 +35,8 @@ export default function SignupIndexRoute() {
return (
<div style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
"justify-content": "center",
"align-items": "center",
height: "100vh",
background: "#07051a"
}}>

View file

@ -1,3 +1,4 @@
/// <reference types="vitest/globals" />
import "@testing-library/jest-dom";
import { beforeAll, afterEach, afterAll } from "vitest";
import { setupServer } from "msw/node";
@ -35,14 +36,16 @@ Object.defineProperty(window, "matchMedia", {
});
// Mock IntersectionObserver
global.IntersectionObserver = class IntersectionObserver {
(global as any).IntersectionObserver = class IntersectionObserver {
root = null;
rootMargin = '';
scrollMargin = '';
thresholds: number[] = [];
constructor() {}
disconnect() {}
observe() {}
takeRecords() {
return [];
}
trigger() {}
unobserve() {}
takeRecords() { return []; }
};
// Mock ResizeObserver

View file

@ -95,7 +95,7 @@ test.describe("AI Chat Widget", () => {
const results = await new AxeBuilder({ page }).analyze();
const criticalViolations = results.violations.filter(
(v: { impact: string }) => v.impact === "critical"
(v: { impact?: string | null }) => v.impact === "critical"
);
expect(criticalViolations).toEqual([]);
});

View file

@ -44,8 +44,8 @@ test.describe("AI API Endpoints", () => {
if (!companyToken) test.skip();
await page.goto(`${API_BASE}/`);
await page.evaluate((t: string) => {
window.sessionStorage.setItem("nxtgauge_access_token", t);
await page.evaluate((t: string | null) => {
window.sessionStorage.setItem("nxtgauge_access_token", t ?? "");
}, companyToken);
const ctx = await request.newContext();

View file

@ -55,7 +55,7 @@ async function registerUser(user: TestUser): Promise<TestUser> {
// Get OTP from Redis
await new Promise(r => setTimeout(r, 500));
const otpCode = await getOTPFromRedis(user.userId);
const otpCode = await getOTPFromRedis(user.userId!);
if (!otpCode) throw new Error("Could not get OTP from Redis");
console.log(` ✅ OTP retrieved: ${otpCode}`);
@ -77,7 +77,7 @@ async function registerUser(user: TestUser): Promise<TestUser> {
const loginData = await loginResponse.json();
if (!loginData.access_token) throw new Error("Login failed");
user.accessToken = loginData.access_token;
console.log(` ✅ Logged in, token length: ${user.accessToken.length}`);
console.log(` ✅ Logged in, token length: ${user.accessToken!.length}`);
return user;
}
@ -97,7 +97,7 @@ async function setupFrontendAuth(page: Page, user: TestUser) {
selectedProfessionalRole: role, name, fullName: name, id: userId
}));
sessionStorage.setItem("nxtgauge_access_token", token);
}, { token: user.accessToken, email: user.email, userId: user.userId, role, name });
}, { token: user.accessToken!, email: user.email, userId: user.userId, role, name });
}
async function takeScreenshot(page: Page, name: string) {

View file

@ -55,7 +55,7 @@ async function registerUser(user: TestUser): Promise<TestUser> {
// Get OTP from Redis
await new Promise(r => setTimeout(r, 500));
const otpCode = await getOTPFromRedis(user.userId);
const otpCode = await getOTPFromRedis(user.userId!);
if (!otpCode) throw new Error("Could not get OTP from Redis");
console.log(` ✅ OTP retrieved: ${otpCode}`);
@ -77,7 +77,7 @@ async function registerUser(user: TestUser): Promise<TestUser> {
const loginData = await loginResponse.json();
if (!loginData.access_token) throw new Error("Login failed");
user.accessToken = loginData.access_token;
console.log(` ✅ Logged in, token length: ${user.accessToken.length}`);
console.log(` ✅ Logged in, token length: ${user.accessToken!.length}`);
return user;
}
@ -97,7 +97,7 @@ async function setupFrontendAuth(page: Page, user: TestUser) {
selectedProfessionalRole: role, name, fullName: name, id: userId
}));
sessionStorage.setItem("nxtgauge_access_token", token);
}, { token: user.accessToken, email: user.email, userId: user.userId, role, name });
}, { token: user.accessToken!, email: user.email, userId: user.userId, role, name });
}
async function takeScreenshot(page: Page, name: string) {

View file

@ -74,7 +74,7 @@ async function registerUser(user: TestUser): Promise<TestUser> {
// Wait for OTP to be generated
await new Promise(r => setTimeout(r, 1000));
const otpCode = await getOTPFromRedis(user.userId);
const otpCode = await getOTPFromRedis(user.userId!);
if (!otpCode) throw new Error("Could not get OTP from Redis");
console.log(` ✅ OTP retrieved: ${otpCode}`);
@ -96,7 +96,7 @@ async function registerUser(user: TestUser): Promise<TestUser> {
const loginData = await loginResponse.json();
if (!loginData.access_token) throw new Error("Login failed");
user.accessToken = loginData.access_token;
console.log(` ✅ Logged in, token length: ${user.accessToken.length}`);
console.log(` ✅ Logged in, token length: ${user.accessToken!.length}`);
return user;
}
@ -116,7 +116,7 @@ async function setupFrontendAuth(page: Page, user: TestUser) {
selectedProfessionalRole: role, name, fullName: name, id: userId
}));
sessionStorage.setItem("nxtgauge_access_token", token);
}, { token: user.accessToken, email: user.email, userId: user.userId, role, name });
}, { token: user.accessToken!, email: user.email, userId: user.userId, role, name });
}
async function takeScreenshot(page: Page, name: string) {

View file

@ -55,7 +55,7 @@ async function registerUser(user: TestUser): Promise<TestUser> {
// Get OTP from Redis
await new Promise(r => setTimeout(r, 500));
const otpCode = await getOTPFromRedis(user.userId);
const otpCode = await getOTPFromRedis(user.userId!);
if (!otpCode) throw new Error("Could not get OTP from Redis");
console.log(` ✅ OTP retrieved: ${otpCode}`);
@ -77,7 +77,7 @@ async function registerUser(user: TestUser): Promise<TestUser> {
const loginData = await loginResponse.json();
if (!loginData.access_token) throw new Error("Login failed");
user.accessToken = loginData.access_token;
console.log(` ✅ Logged in, token length: ${user.accessToken.length}`);
console.log(` ✅ Logged in, token length: ${user.accessToken!.length}`);
return user;
}
@ -97,7 +97,7 @@ async function setupFrontendAuth(page: Page, user: TestUser) {
selectedProfessionalRole: role, name, fullName: name, id: userId
}));
sessionStorage.setItem("nxtgauge_access_token", token);
}, { token: user.accessToken, email: user.email, userId: user.userId, role, name });
}, { token: user.accessToken!, email: user.email, userId: user.userId, role, name });
}
async function takeScreenshot(page: Page, name: string) {

View file

@ -209,7 +209,7 @@ async function runSignup(page: any, scenario: SignupScenario, email: string) {
async function seedSession(page: any, scenario: SignupScenario, email: string) {
await page.evaluate(
({ role, mail }) => {
({ role, mail }: { role: string; mail: string }) => {
sessionStorage.setItem("nxtgauge_access_token", "pw-test-token");
const payload = {
email: mail,

8
tests/tsconfig.json Normal file
View file

@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"moduleResolution": "node",
"types": ["vite/client", "node"]
},
"include": ["./**/*.ts", "./**/*.tsx"]
}

View file

@ -1,6 +1,6 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent, waitFor } from "@solidjs/testing-library";
import { AiChatWidget } from "../../src/components/AiChatWidget";
import { AiChatWidget } from "~/components/AiChatWidget";
global.fetch = vi.fn();
@ -51,7 +51,7 @@ describe("AiChatWidget", () => {
fireEvent.click(button);
await waitFor(() => {
const input = screen.getByPlaceholder(/Ask/i);
const input = screen.getByPlaceholderText(/Ask/i);
fireEvent.change(input, { target: { value: "Test" } });
fireEvent.keyDown(input, { key: "Enter" });
});
@ -77,7 +77,7 @@ describe("AiChatWidget", () => {
fireEvent.click(button);
await waitFor(() => {
const input = screen.getByPlaceholder(/Ask/i);
const input = screen.getByPlaceholderText(/Ask/i);
fireEvent.change(input, { target: { value: "Hello" } });
fireEvent.keyDown(input, { key: "Enter" });
});

View file

@ -13,7 +13,7 @@ export default defineConfig({
"/api/gateway": {
target: "http://localhost:9100",
changeOrigin: true,
rewrite: (path) =>
rewrite: (path: string) =>
path
.replace(/^\/api\/gateway\/api(\/|$)/, "/api$1")
.replace(/^\/api\/gateway(\/|$)/, "/api$1"),