Repository navigation
🤖 perf: add the static first-run view's markup as a server constant (T3 C step 2 PR 2) #6444
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
6e3cf1a
🤖 tests: pin the static first-run view to the live gateway step (T3 C…
ThomasK33 e812b9e
🤖 perf: add the static first-run view's markup as a server constant (…
ThomasK33 4de5a0b
🤖 fix: give the static first-run view's shell the viewport height (PR…
ThomasK33 1a76083
🤖 fix: stack the static first-run view on the live overlay's layer (P…
ThomasK33 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| import { FIRST_RUN_VIEW_ID } from "@/common/constants/firstRunView"; | ||
|
|
||
| /** | ||
| * The static first-run view (T3 #5971, Option C step 2, plan section 5.6): a copy of the onboarding | ||
| * wizard's gateway step ("Xum Gateway (evaluation credits)"), the step a new server opens on. On `/`, | ||
| * the server will put it in a <template> and an early script will show it before the app JS runs | ||
| * (PR 4). Until then nothing serves it. The live wizard removes it once it shows the same step. | ||
| * | ||
| * Captured from the live page at 412 px (61 elements), then edited: | ||
| * - The app shell's <main> is a <div>. The boot loader's <main> in #root stays the only landmark. | ||
| * - The shell's root is `h-full`: outside #root, which globals.css sizes to the viewport, its | ||
| * nested `h-full` would resolve against an auto height and collapse to the content height. | ||
| * - Radix runtime artifacts are gone: focus guards, generated ids, aria-hidden, tabindex, | ||
| * pointer-events. The disabled attributes are gone too: the live buttons are enabled, and | ||
| * `disabled:opacity-50` made the copy look disabled. | ||
| * - The root is `inert`, so no part of the copy takes focus or clicks before the handoff. | ||
| * - The root is `fixed`, so it is a stacking context: its z-index is the live overlay's 1500 | ||
| * (`z-[1500]`), or the iOS standalone `.ios-status-bar-band` (1001) would paint over the copy. | ||
| * tests/ui/onboarding/firstRunViewParity.test.ts fails when the overlay or the dialog drifts from | ||
| * the live wizard in tags, classes or text. | ||
| * | ||
| * SECURITY AUDIT: trusted server text. Every byte is fixed at build time: it holds no user, | ||
| * request, config or repo data, and nothing may be interpolated into it but FIRST_RUN_VIEW_ID. | ||
| * PR 4 inserts it with `template.content.cloneNode(true)`, never with innerHTML, outerHTML or | ||
| * insertAdjacentHTML. | ||
| */ | ||
| export const FIRST_RUN_VIEW_HTML = `<div id="${FIRST_RUN_VIEW_ID}" inert style="position:fixed;inset:0;z-index:1500;overflow:hidden"> | ||
| <div class="h-full"> | ||
| <div class="bg-surface-primary h-full"> | ||
| <div class="bg-surface-primary mobile-layout flex h-full overflow-hidden pt-[env(safe-area-inset-top)] pr-[env(safe-area-inset-right)] pl-[env(safe-area-inset-left)]"> | ||
| <div class="mobile-main-content flex min-w-0 flex-1 flex-col overflow-hidden"> | ||
| <div class="mobile-layout flex flex-1 overflow-hidden"> | ||
| <div class="bg-surface-primary flex flex-1 flex-col overflow-hidden"> | ||
| <div class="bg-sidebar border-border-light flex shrink-0 items-center border-b px-4 py-2 "> | ||
| <button type="button" class="border-border-medium bg-background-secondary text-foreground hover:bg-hover rounded-md border px-3 py-1.5 text-sm transition-colors " aria-label="Open sidebar menu">Open sidebar</button> | ||
| </div> | ||
| <div class="flex flex-1 items-center justify-center px-6"> | ||
| <p class="text-content-secondary text-sm">Select or add a project to get started.</p> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| <div data-modal-dialog-overlay="" class="fixed inset-0 z-[1500] before:absolute before:inset-0 before:bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"></div> | ||
| <div role="dialog" class="bg-dark border-border fixed top-[50%] left-[50%] z-[1500] grid w-[90%] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] max-h-[85vh] grid-rows-[auto_minmax(0,1fr)_auto]" style="max-width: 500px"> | ||
| <div class="flex flex-col space-y-1.5 text-left"> | ||
| <h2 class="text-foreground text-lg font-semibold leading-none tracking-tight">Xum Gateway (evaluation credits)</h2> | ||
| </div> | ||
| <div class="min-h-0 overflow-y-auto"> | ||
| <div class="text-content-secondary flex flex-col gap-4"> | ||
| <div class="flex items-center justify-end gap-2"> | ||
| <span class="text-content-secondary text-xs" aria-hidden="true">1 / 7</span> | ||
| <div class="flex items-center gap-1" role="img" aria-label="Step 1 of 7"> | ||
| <span class="h-1.5 w-1.5 rounded-full bg-accent"></span> | ||
| <span class="h-1.5 w-1.5 rounded-full bg-border-medium"></span> | ||
| <span class="h-1.5 w-1.5 rounded-full bg-border-medium"></span> | ||
| <span class="h-1.5 w-1.5 rounded-full bg-border-medium"></span> | ||
| <span class="h-1.5 w-1.5 rounded-full bg-border-medium"></span> | ||
| <span class="h-1.5 w-1.5 rounded-full bg-border-medium"></span> | ||
| <span class="h-1.5 w-1.5 rounded-full bg-border-medium"></span> | ||
| </div> | ||
| </div> | ||
| <div class="flex flex-col gap-3"> | ||
| <div class="text-foreground flex items-center gap-2 text-sm font-medium"> | ||
| <span class="bg-accent/10 text-accent inline-flex h-8 w-8 items-center justify-center rounded-md"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewbox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-sparkles h-4 w-4" aria-hidden="true"><path d="M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z"></path><path d="M20 2v4"></path><path d="M22 4h-4"></path><circle cx="4" cy="20" r="2"></circle></svg></span> | ||
| <span>Xum Gateway (evaluation credits)</span> | ||
| </div> | ||
| <div class="text-content-secondary flex flex-col gap-3 text-sm"> | ||
| <p>Xum Gateway enables you to use free AI tokens from <a href="https://coder.com" target="_blank" rel="noopener noreferrer" class="text-accent underline">Coder</a>.</p> | ||
| <p>OSS contributors with GitHub accounts older than 12 months (or GitHub Pro members) can use this to get free evaluation credits.</p> | ||
| <div class="mt-3 flex flex-wrap gap-2"> | ||
| <button class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all active:scale-[0.98] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-accent text-accent-foreground shadow hover:bg-accent-button-hover h-9 px-4 py-2">Login with Xum Gateway</button> | ||
| </div> | ||
| <p class="mt-3">You can also receive those credits through:</p> | ||
| <ul class="ml-4 list-disc space-y-1"> | ||
| <li>early adopters can request credits tied to their GH logins on our <a href="https://discord.gg/VfZXvtnR" target="_blank" rel="noopener noreferrer" class="text-accent underline">Discord</a></li> | ||
| <li>vouchers which you can <a href="https://gateway.xum.cdr.dev" target="_blank" rel="noopener noreferrer" class="text-accent underline">claim here</a></li> | ||
| </ul> | ||
| <p class="mt-3">You can enable this in <button type="button" class="text-accent underline">Settings → Providers</button>.</p> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| <div class="flex gap-3 pt-4 justify-between"> | ||
| <div></div> | ||
| <div class="flex items-center gap-3"> | ||
| <button class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all active:scale-[0.98] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-accent text-accent-foreground shadow hover:bg-accent-button-hover h-9 px-4 py-2 min-w-24">Next</button> | ||
| <button class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all active:scale-[0.98] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-border-medium text-foreground shadow-sm hover:bg-border-darker h-9 px-4 py-2 min-w-24">Skip</button> | ||
| </div> | ||
| </div> | ||
| <button type="button" class="text-muted hover:text-foreground absolute top-4 right-4 rounded-sm transition-colors focus:outline-none disabled:pointer-events-none"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewbox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x h-4 w-4" aria-hidden="true"><path d="M18 6 6 18"></path><path d="m6 6 12 12"></path></svg><span class="sr-only">Close</span></button> | ||
| </div> | ||
| </div>`; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| /** | ||
| * The server's static first-run view must match the live wizard's gateway step (T3 #5971, C step 2, | ||
| * plan section 5.6, PR 2). The live wizard replaces the static copy in one commit, so any drift in | ||
| * tags, classes or text shows as a jump at handoff. Catches, for example, a class changed on the | ||
| * live step (OnboardingWizardSplash) without the same change in FIRST_RUN_VIEW_HTML. | ||
| * | ||
| * The test renders OnboardingWizardSplash directly: SplashScreenProvider shows no splash in test | ||
| * mode (DISABLE_SPLASH_SCREENS, SplashScreens/index.ts). | ||
| */ | ||
| import "../dom"; | ||
| import React from "react"; | ||
| import { act, cleanup, render } from "@testing-library/react"; | ||
|
|
||
| let mockProviders: { config: Record<string, unknown> | null; loading: boolean }; | ||
|
|
||
| jest.mock("@/browser/hooks/useProvidersConfig", () => ({ | ||
| useProvidersConfig: () => ({ | ||
| ...mockProviders, | ||
| refresh: jest.fn(), | ||
| updateOptimistically: jest.fn(), | ||
| }), | ||
| })); | ||
| jest.mock("@/browser/features/SplashScreens/SplashScreenProvider", () => ({ | ||
| ...jest.requireActual<typeof import("@/browser/features/SplashScreens/SplashScreenProvider")>( | ||
| "@/browser/features/SplashScreens/SplashScreenProvider" | ||
| ), | ||
| useOnboardingPause: () => ({ paused: null, pause: jest.fn(), resume: jest.fn() }), | ||
| })); | ||
| jest.mock("@/browser/contexts/SettingsContext", () => ({ | ||
| useSettings: () => ({ open: jest.fn(), registerOnClose: () => () => undefined }), | ||
| })); | ||
| jest.mock("@/browser/contexts/ProjectContext", () => ({ | ||
| useProjectContext: () => ({ addProject: jest.fn(), userProjects: new Map() }), | ||
| })); | ||
| jest.mock("@/browser/contexts/API", () => ({ | ||
| useAPI: () => ({ api: null }), | ||
| useLiveAPI: () => null, | ||
| })); | ||
| jest.mock("@/browser/hooks/useRouting", () => ({ | ||
| useRouting: () => ({ navigateToProject: jest.fn() }), | ||
| })); | ||
| jest.mock("@/browser/hooks/useMuxGatewayAccountStatus", () => ({ | ||
| ...jest.requireActual<typeof import("@/browser/hooks/useMuxGatewayAccountStatus")>( | ||
| "@/browser/hooks/useMuxGatewayAccountStatus" | ||
| ), | ||
| useMuxGatewayAccountStatus: () => ({ | ||
| data: null, | ||
| error: null, | ||
| isLoading: false, | ||
| refresh: jest.fn(), | ||
| }), | ||
| })); | ||
|
|
||
| import { OnboardingWizardSplash } from "@/browser/features/SplashScreens/OnboardingWizardSplash"; | ||
| import { FIRST_RUN_VIEW_ID } from "@/common/constants/firstRunView"; | ||
| import { FIRST_RUN_VIEW_HTML } from "@/node/orpc/firstRunView"; | ||
|
|
||
| /** Every element in document order: tag, class attribute and its own text, whitespace-collapsed. */ | ||
| function shape(root: Element): string[] { | ||
| const norm = (s: string) => s.replace(/\s+/g, " ").trim(); | ||
| return [root, ...Array.from(root.querySelectorAll("*"))].map((el) => { | ||
| const text = Array.from(el.childNodes) | ||
| .filter((n) => n.nodeType === window.Node.TEXT_NODE) | ||
| .map((n) => n.textContent ?? "") | ||
| .join(""); | ||
| return `<${el.tagName.toLowerCase()} class="${norm(el.getAttribute("class") ?? "")}">${norm(text)}`; | ||
| }); | ||
| } | ||
|
|
||
| /** Parses the constant the way the page will: from a <template>, cloned with content.cloneNode. */ | ||
| function staticView(): HTMLElement { | ||
| const doc = new window.DOMParser().parseFromString( | ||
| `<template>${FIRST_RUN_VIEW_HTML}</template>`, | ||
| "text/html" | ||
| ); | ||
| const fragment = doc.querySelector("template")!.content.cloneNode(true) as DocumentFragment; | ||
| expect(fragment.children).toHaveLength(1); | ||
| return document.importNode(fragment.firstElementChild!, true) as HTMLElement; | ||
| } | ||
|
|
||
| afterEach(() => { | ||
| cleanup(); | ||
| document.body.innerHTML = ""; | ||
| }); | ||
|
|
||
| test("the static view is one inert element with the shared id and no main landmark", () => { | ||
| const view = staticView(); | ||
| expect(view.id).toBe(FIRST_RUN_VIEW_ID); | ||
| expect(view.hasAttribute("inert")).toBe(true); | ||
| expect(view.querySelector("main")).toBeNull(); // the boot loader's <main> stays the only one | ||
| }); | ||
|
|
||
| test("the static view matches the live gateway step's overlay and dialog", () => { | ||
| const view = staticView(); | ||
| const expected = (selector: string) => shape(view.querySelector(selector)!); | ||
| const staticOverlay = expected("[data-modal-dialog-overlay]"); | ||
| const staticDialog = expected("[role=dialog]"); | ||
|
|
||
| // Under the static view, the live wizard opens on the gateway step and removes it (PR 1). | ||
| document.body.append(view); | ||
| mockProviders = { config: null, loading: true }; | ||
| const wizard = () => React.createElement(OnboardingWizardSplash, { onDismiss: jest.fn() }); | ||
| const rendered = render(wizard()); | ||
| mockProviders = { config: {}, loading: false }; | ||
| act(() => rendered.rerender(wizard())); | ||
| expect(document.getElementById(FIRST_RUN_VIEW_ID)).toBeNull(); | ||
|
|
||
| const live = (selector: string) => shape(document.body.querySelector(selector)!); | ||
| expect(live("[data-modal-dialog-overlay]")).toEqual(staticOverlay); | ||
| expect(live("[role=dialog]")).toEqual(staticDialog); | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.