This commit is contained in:
abhinav7x94 2026-08-26 04:03:36 +05:30 committed by GitHub
commit 08152b86e5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 389 additions and 51 deletions

View file

@ -29,5 +29,9 @@ jobs:
- name: Run TypeScript type checking
run: bunx turbo run check-types --filter='@supermemory/ai-sdk' --filter='@supermemory/memory-graph'
- name: Run web tests
working-directory: apps/web
run: bun run test
- name: Run Biome CI (format & lint on changed files)
run: bunx biome ci --changed --since=origin/main --no-errors-on-unmatched

View file

@ -0,0 +1,308 @@
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"
import { GlobalWindow } from "happy-dom"
import React from "react"
const browserWindow = new GlobalWindow({
url: "https://app.supermemory.ai/login",
})
Object.assign(globalThis, {
HTMLElement: browserWindow.HTMLElement,
MutationObserver: browserWindow.MutationObserver,
Node: browserWindow.Node,
document: browserWindow.document,
getComputedStyle: browserWindow.getComputedStyle.bind(browserWindow),
localStorage: browserWindow.localStorage,
navigator: browserWindow.navigator,
window: browserWindow,
})
type SignInResult = {
data: unknown
error: unknown
}
type SignInOptions = Record<string, unknown>
type SignInImplementation = (options: SignInOptions) => Promise<SignInResult>
const successResult: SignInResult = {
data: { redirect: true, url: "https://accounts.example.com" },
error: null,
}
let socialImplementation: SignInImplementation = async () => successResult
let oauth2Implementation: SignInImplementation = async () => successResult
const socialSignIn = mock((options: SignInOptions) =>
socialImplementation(options),
)
const oauth2SignIn = mock((options: SignInOptions) =>
oauth2Implementation(options),
)
const capture = mock(() => {})
mock.module("@lib/auth", () => ({
signIn: {
email: mock(async () => successResult),
magicLink: mock(async () => successResult),
oauth2: oauth2SignIn,
social: socialSignIn,
},
useSession: () => ({ data: null, isPending: false }),
}))
mock.module("@lib/posthog", () => ({
usePostHog: () => ({ capture }),
}))
mock.module("next/navigation", () => ({
useRouter: () => ({ push: mock(() => {}) }),
useSearchParams: () => new URLSearchParams(),
}))
mock.module("motion/react", () => ({
motion: {
div: ({
animate: _animate,
children,
initial: _initial,
transition: _transition,
...props
}: React.ComponentProps<"div"> & {
animate?: unknown
initial?: unknown
transition?: unknown
}) => React.createElement("div", props, children),
},
}))
mock.module("@/components/initial-header", () => ({
InitialHeader: () => React.createElement("header"),
}))
mock.module("@/components/login-tools-panel", () => ({
LoginToolsPanel: () => React.createElement("aside"),
}))
mock.module("@/lib/fonts", () => ({
dmSansClassName: () => "",
}))
process.env.NEXT_PUBLIC_HOST_ID = "supermemory"
const { act, cleanup, fireEvent, render, screen } = await import(
"@testing-library/react"
)
const LoginPage = (await import("./page")).default
const pendingMethodKey = "supermemory-pending-login-method"
const pendingTimestampKey = "supermemory-pending-login-timestamp"
function renderLoginPage() {
return render(React.createElement(LoginPage))
}
beforeEach(() => {
browserWindow.localStorage.clear()
socialSignIn.mockClear()
oauth2SignIn.mockClear()
capture.mockClear()
socialImplementation = async () => successResult
oauth2Implementation = async () => successResult
})
afterEach(() => {
cleanup()
})
describe("external provider sign-in", () => {
const providerCases = [
{
buttonName: "Continue with Google",
expectedOptions: { provider: "google" },
label: "Google",
method: "google",
useOauth2: false,
},
{
buttonName: "Continue with Github",
expectedOptions: { provider: "github" },
label: "GitHub",
method: "github",
useOauth2: false,
},
{
buttonName: "Continue with AgentID",
expectedOptions: { providerId: "agentid" },
label: "AgentID",
method: "agentid",
useOauth2: true,
},
] as const
for (const providerCase of providerCases) {
test(`handles a fulfilled ${providerCase.label} error`, async () => {
const providerError = {
message: `${providerCase.label} is temporarily unavailable`,
status: 503,
statusText: "Service Unavailable",
}
const implementation: SignInImplementation = async () => ({
data: null,
error: providerError,
})
if (providerCase.useOauth2) {
oauth2Implementation = implementation
} else {
socialImplementation = implementation
}
renderLoginPage()
fireEvent.click(
screen.getByRole("button", {
name: new RegExp(providerCase.buttonName, "i"),
}),
)
const alert = await screen.findByText(providerError.message)
expect(alert.getAttribute("role")).toBe("alert")
expect(screen.queryByText(/Redirecting/)).toBeNull()
expect(browserWindow.localStorage.getItem(pendingMethodKey)).toBeNull()
expect(browserWindow.localStorage.getItem(pendingTimestampKey)).toBeNull()
const providerSignIn = providerCase.useOauth2
? oauth2SignIn
: socialSignIn
expect(providerSignIn).toHaveBeenCalledTimes(1)
expect(providerSignIn.mock.calls[0]?.[0]).toMatchObject({
callbackURL: "https://app.supermemory.ai/?extension-auth-success=true",
...providerCase.expectedOptions,
})
})
}
const fulfilledNetworkCases = [
{
buttonName: "Continue with Google",
label: "Google social",
useOauth2: false,
},
{
buttonName: "Continue with AgentID",
label: "AgentID OAuth2",
useOauth2: true,
},
] as const
for (const networkCase of fulfilledNetworkCases) {
test(`normalizes a fulfilled status-0 ${networkCase.label} error`, async () => {
const implementation: SignInImplementation = async () => ({
data: null,
error: { status: 0, statusText: "" },
})
if (networkCase.useOauth2) {
oauth2Implementation = implementation
} else {
socialImplementation = implementation
}
renderLoginPage()
const button = screen.getByRole("button", {
name: new RegExp(networkCase.buttonName, "i"),
})
fireEvent.click(button)
const alert = await screen.findByText(
"Network error. Please check your connection and try again.",
)
expect(alert.getAttribute("role")).toBe("alert")
expect(screen.queryByText(/Redirecting/)).toBeNull()
expect(button.hasAttribute("disabled")).toBe(false)
expect(browserWindow.localStorage.getItem(pendingMethodKey)).toBeNull()
expect(browserWindow.localStorage.getItem(pendingTimestampKey)).toBeNull()
const providerSignIn = networkCase.useOauth2 ? oauth2SignIn : socialSignIn
expect(providerSignIn).toHaveBeenCalledTimes(1)
})
}
test("keeps a nonzero provider error whose message looks network-like", async () => {
const providerMessage = "Failed to fetch GitHub account details"
socialImplementation = async () => ({
data: null,
error: {
message: providerMessage,
status: 503,
statusText: "Service Unavailable",
},
})
renderLoginPage()
const button = screen.getByRole("button", {
name: /Continue with Github/i,
})
fireEvent.click(button)
const alert = await screen.findByText(providerMessage)
expect(alert.getAttribute("role")).toBe("alert")
expect(
screen.queryByText(
"Network error. Please check your connection and try again.",
),
).toBeNull()
expect(button.hasAttribute("disabled")).toBe(false)
expect(browserWindow.localStorage.getItem(pendingMethodKey)).toBeNull()
expect(browserWindow.localStorage.getItem(pendingTimestampKey)).toBeNull()
})
test("normalizes a rejected network error and clears pending state", async () => {
socialImplementation = async () => {
throw new TypeError("Failed to fetch")
}
renderLoginPage()
fireEvent.click(
screen.getByRole("button", { name: /Continue with Google/i }),
)
const alert = await screen.findByText(
"Network error. Please check your connection and try again.",
)
expect(alert.getAttribute("role")).toBe("alert")
expect(screen.queryByText(/Redirecting/)).toBeNull()
expect(browserWindow.localStorage.getItem(pendingMethodKey)).toBeNull()
expect(browserWindow.localStorage.getItem(pendingTimestampKey)).toBeNull()
})
test("keeps loading and pending state after a successful handoff", async () => {
let resolveSignIn: ((result: SignInResult) => void) | undefined
socialImplementation = () =>
new Promise((resolve) => {
resolveSignIn = resolve
})
renderLoginPage()
fireEvent.click(
screen.getByRole("button", { name: /Continue with Google/i }),
)
expect(await screen.findByText(/Redirecting/)).toBeTruthy()
expect(browserWindow.localStorage.getItem(pendingMethodKey)).toBe("google")
expect(browserWindow.localStorage.getItem(pendingTimestampKey)).toMatch(
/^\d+$/,
)
await act(async () => {
resolveSignIn?.(successResult)
await Promise.resolve()
})
expect(screen.getByText(/Redirecting/)).toBeTruthy()
expect(browserWindow.localStorage.getItem(pendingMethodKey)).toBe("google")
expect(browserWindow.localStorage.getItem(pendingTimestampKey)).toMatch(
/^\d+$/,
)
})
})

View file

@ -53,6 +53,13 @@ function buildMcpAuthorizeResumeUrl(
return `${backend}/api/auth/oauth2/authorize?${p.toString()}`
}
function clearPendingLoginMethod() {
try {
localStorage.removeItem("supermemory-pending-login-method")
localStorage.removeItem("supermemory-pending-login-timestamp")
} catch {}
}
function LoginHeadline({ className }: { className?: string }) {
return (
<div className={cn("max-w-sm text-center", className)}>
@ -204,9 +211,32 @@ export default function LoginPage() {
} catch {}
}
function getErrorText(error: unknown): string | null {
if (error instanceof Error) return error.message
if (
typeof error === "object" &&
error !== null &&
"message" in error &&
typeof error.message === "string"
) {
return error.message
}
return null
}
function getPlainObjectStatus(error: unknown): number | null {
if (typeof error !== "object" || error === null) return null
const prototype = Object.getPrototypeOf(error)
if (prototype !== Object.prototype && prototype !== null) return null
if (!("status" in error) || typeof error.status !== "number") return null
return error.status
}
function isNetworkError(error: unknown): boolean {
if (!(error instanceof Error)) return false
const message = error.message.toLowerCase()
const status = getPlainObjectStatus(error)
if (status !== null) return status === 0
const message = getErrorText(error)?.toLowerCase()
if (!message) return false
return (
message.includes("load failed") ||
message.includes("networkerror") ||
@ -219,19 +249,40 @@ export default function LoginPage() {
if (isNetworkError(error)) {
return "Network error. Please check your connection and try again."
}
if (error instanceof Error) {
return error.message
return (
getErrorText(error) || "An unexpected error occurred. Please try again."
)
}
async function handleExternalSignIn(
provider: "agentid" | "github" | "google",
startSignIn: () => Promise<{ error?: unknown }>,
) {
if (loadingMessage) return
setError(null)
setIsLoading(true)
posthog.capture("login_attempt", {
method: "social",
provider,
})
setPendingLoginMethod(provider)
try {
const result = await startSignIn()
if (!result.error) return
setError(getErrorMessage(result.error))
} catch (error) {
setError(getErrorMessage(error))
}
return "An unexpected error occurred. Please try again."
setIsLoading(false)
clearPendingLoginMethod()
}
// If we land back on this page with an error, clear any pending marker
useEffect(() => {
if (params.get("error")) {
try {
localStorage.removeItem("supermemory-pending-login-method")
localStorage.removeItem("supermemory-pending-login-timestamp")
} catch {}
clearPendingLoginMethod()
}
}, [params])
@ -512,22 +563,12 @@ export default function LoginPage() {
className="w-full"
disabled={Boolean(loadingMessage)}
onClick={() => {
if (loadingMessage) return
setIsLoading(true)
posthog.capture("login_attempt", {
method: "social",
provider: "google",
})
setPendingLoginMethod("google")
signIn
.social({
void handleExternalSignIn("google", () =>
signIn.social({
callbackURL: getCallbackURL(),
provider: "google",
})
.catch((err: unknown) => {
setError(getErrorMessage(err))
setIsLoading(false)
})
}),
)
}}
/>
</div>
@ -571,22 +612,12 @@ export default function LoginPage() {
className="w-full"
disabled={Boolean(loadingMessage)}
onClick={() => {
if (loadingMessage) return
setIsLoading(true)
posthog.capture("login_attempt", {
method: "social",
provider: "github",
})
setPendingLoginMethod("github")
signIn
.social({
void handleExternalSignIn("github", () =>
signIn.social({
callbackURL: getCallbackURL(),
provider: "github",
})
.catch((err: unknown) => {
setError(getErrorMessage(err))
setIsLoading(false)
})
}),
)
}}
/>
</div>
@ -644,22 +675,12 @@ export default function LoginPage() {
className="w-full"
disabled={Boolean(loadingMessage)}
onClick={() => {
if (loadingMessage) return
setIsLoading(true)
posthog.capture("login_attempt", {
method: "social",
provider: "agentid",
})
setPendingLoginMethod("agentid")
signIn
.oauth2({
void handleExternalSignIn("agentid", () =>
signIn.oauth2({
callbackURL: getCallbackURL(),
providerId: "agentid",
})
.catch((err: unknown) => {
setError(getErrorMessage(err))
setIsLoading(false)
})
}),
)
}}
/>
</div>

View file

@ -13,6 +13,7 @@
"check-types": "tsc --noEmit",
"start": "next start",
"lint": "biome check --write",
"test": "bun test",
"preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview",
"deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
"upload": "opennextjs-cloudflare build && opennextjs-cloudflare upload",
@ -119,12 +120,14 @@
"@biomejs/biome": "^2.2.2",
"@sentry/cli": "^2.52.0",
"@tailwindcss/postcss": "^4.1.11",
"@testing-library/react": "^16.3.2",
"@total-typescript/tsconfig": "^1.0.4",
"@types/canvas-confetti": "^1.9.0",
"@types/is-hotkey": "^0.1.10",
"@types/node": "^24.0.4",
"@types/react": "^19.2.9",
"@types/react-dom": "^19.2.3",
"happy-dom": "^20.9.0",
"tailwindcss": "^4.1.11",
"typescript": "^5.8.3",
"wrangler": "^4.26.0"

View file

@ -244,12 +244,14 @@
"@biomejs/biome": "^2.2.2",
"@sentry/cli": "^2.52.0",
"@tailwindcss/postcss": "^4.1.11",
"@testing-library/react": "^16.3.2",
"@total-typescript/tsconfig": "^1.0.4",
"@types/canvas-confetti": "^1.9.0",
"@types/is-hotkey": "^0.1.10",
"@types/node": "^24.0.4",
"@types/react": "^19.2.9",
"@types/react-dom": "^19.2.3",
"happy-dom": "^20.9.0",
"tailwindcss": "^4.1.11",
"typescript": "^5.8.3",
"wrangler": "^4.26.0",