// Opening an artifact shows ONE loading surface, not three.
//
// ## What went wrong
//
// Opening an artifact used to walk through three visually distinct placeholders
// in quick succession, each in its own layout and each visible for a few hundred
// milliseconds:
//
// 1. "Loading artifact…" — the row being fetched, small corner text
// 2. "Preparing the renderer…" — the lazy chunk arriving, a different corner
// 3. "Preparing interactive UI" — the shell booting, a card in the middle
//
// Those are executor's own internal boundaries — an HTTP request, a code-split
// point, an iframe handshake — and none of them is a fact a person opening a
// saved dashboard has any use for. What they produced was churn: three
// different things flashing in three different places. Three quick loading
// states are worse than one longer one, because every transition is a fresh
// demand on the eye.
//
// ## What is asserted, and why it is asserted this way
//
// The honest test of "the user never saw a placeholder" is not a screenshot at
// one moment — the states this is about were only ever visible for a few
// hundred milliseconds, so any single sample would miss them and pass against
// the old code too. So the page is SAMPLED CONTINUOUSLY, from before navigation
// until the artifact is live, and the assertion is over everything that was ever
// on screen:
//
// - none of the three placeholder texts ever appeared;
// - the stage's bounding box never moved, so nothing was ever laid out twice.
//
// Both directions are covered, because they fail differently: warm (from the
// gallery, renderer chunk already fetched) and cold (a direct URL in a fresh
// context, nothing cached).
import { randomBytes } from "node:crypto";
import { expect } from "@effect/vitest";
import { Effect, Predicate } from "effect";
import type { Page } from "playwright";
import { composePluginApi } from "@executor-js/api/server";
import { AccountHttpApi } from "@executor-js/api";
import type { ArtifactId } from "@executor-js/sdk/shared";
import { scenario } from "../src/scenario";
import { Api, Browser, Mcp, Target } from "../src/services";
import { visit } from "../src/surfaces/browser";
const api = composePluginApi([] as const);
const uniqueSuffix = () => randomBytes(4).toString("hex");
/**
* The placeholder texts that must never reach a user again.
*
* Named literally rather than by testid: the point is that the WORDS are gone
* from the experience, and a rename that kept the churn under a different
* caption should not pass. Each string is the exact one the corresponding
* placeholder rendered before this change.
*/
const FORBIDDEN_LOADING_TEXT = [
"Loading artifact",
"Preparing the renderer",
"Preparing interactive UI",
] as const;
/** A tall, distinctive artifact — the same shape the main artifacts scenario
* uses, so the fill-mode sizing under test here is the real one. */
const artifactSource = (marker: string) => `
function App() {
return (
Loading Surface
${marker}
{Array.from({ length: 30 }, (_, i) => (
Row {i + 1}: ${marker}
))}
);
}
`;
const structuredOf = (result: { readonly raw: unknown }): Record =>
((result.raw as { structuredContent?: Record }).structuredContent ??
{}) as Record;
const artifactContent = (page: Page) =>
page.frameLocator('[data-testid="artifact-shell-frame"]').frameLocator("iframe");
type Sample = {
readonly text: string;
readonly stage: {
readonly x: number;
readonly y: number;
readonly w: number;
readonly h: number;
} | null;
};
declare global {
// eslint-disable-next-line no-var
var __loadingSamples: Array | undefined;
// eslint-disable-next-line no-var
var __loadingSampler: number | undefined;
}
/**
* Watch the CONSOLE document continuously for the whole open.
*
* Installed as an init script so it is running before the first byte of the
* page, and polls on an animation frame — fast enough that a placeholder
* visible for even one paint is recorded. `innerText` of the console document
* only (never the frames): the shell's own inner states live one document down
* and are covered separately, and reaching into a frame from here would sample
* the artifact's own text as if it were the console's.
*
* The stage's box is sampled alongside, because a placeholder that came and
* went without changing any TEXT would still have moved the layout, and a
* layout that jumps is the same defect wearing a different hat.
*/
const startSampling = async (page: Page): Promise => {
await page.addInitScript(() => {
globalThis.__loadingSamples = [];
const sample = () => {
const stage = document.querySelector(
'[data-testid="artifact-shell-container"], [data-slot="artifact-stage-skeleton"]',
);
const box = stage?.getBoundingClientRect();
globalThis.__loadingSamples?.push({
text: document.body?.innerText ?? "",
stage: box
? {
x: Math.round(box.x),
y: Math.round(box.y),
w: Math.round(box.width),
h: Math.round(box.height),
}
: null,
});
globalThis.__loadingSampler = requestAnimationFrame(sample);
};
sample();
});
};
const readSamples = (page: Page): Promise> =>
page.evaluate(() => globalThis.__loadingSamples ?? []);
/**
* Assert the whole open was one surface.
*
* Two independent properties over the same recording — no placeholder words
* ever rendered, and the stage never changed size or position — because either
* one alone would let the churn back in through the other door.
*/
const expectSingleLoadingSurface = (samples: ReadonlyArray, label: string): void => {
expect(samples.length, `${label}: the sampler actually ran`).toBeGreaterThan(3);
for (const forbidden of FORBIDDEN_LOADING_TEXT) {
const hit = samples.findIndex((entry) => entry.text.includes(forbidden));
expect(
hit,
`${label}: "${forbidden}" was on screen at sample ${hit} of ${samples.length} — the open still walks through more than one loading state`,
).toBe(-1);
}
// The stage's geometry, over every frame in which a stage existed at all.
// The skeleton and the artifact frame share one box by construction (the
// skeleton is an absolutely-positioned cover over the frame's own container),
// so a change here means one of them was laid out differently from the other.
const boxes = samples.map((entry) => entry.stage).filter(Predicate.isNotNull);
expect(boxes.length, `${label}: the stage was on screen at some point`).toBeGreaterThan(0);
const first = boxes[0];
if (!first) return;
for (const [index, box] of boxes.entries()) {
// A pixel of tolerance for sub-pixel rounding as the scrollbar settles.
expect(
Math.abs(box.w - first.w) <= 1 && Math.abs(box.h - first.h) <= 1,
`${label}: the stage changed size mid-load at sample ${index} (${JSON.stringify(box)} vs ${JSON.stringify(first)}) — the artifact appeared in a different box than the skeleton held`,
).toBe(true);
}
};
scenario(
"Artifacts · opening an artifact shows one loading surface, not three",
{ timeout: 240_000 },
Effect.gen(function* () {
const target = yield* Target;
const mcp = yield* Mcp;
const browser = yield* Browser;
const { client: apiClient } = yield* Api;
const identity = yield* target.newIdentity();
const client = yield* apiClient(api, identity);
// Artifacts are on by default; the plain session carries the surface
// this scenario exercises.
const session = mcp.session(identity);
const suffix = uniqueSuffix();
const title = `Loading Surface ${suffix}`;
const marker = `surface-ok-${suffix}`;
let artifactId: ArtifactId | undefined;
yield* Effect.gen(function* () {
const created = yield* session.call("create-artifact", {
code: artifactSource(marker),
title,
description: "An artifact opened to watch how it loads",
});
expect(created.ok, `create-artifact succeeded: ${created.text}`).toBe(true);
artifactId = structuredOf(created).artifactId as ArtifactId;
expect(artifactId, "the artifact was persisted").toBeTruthy();
const accountClient = yield* apiClient(AccountHttpApi, identity);
const me = yield* accountClient.account.me();
const orgSlug = me.organization?.slug;
const detailPath = orgSlug
? `/${orgSlug}/artifacts/${artifactId}`
: `/artifacts/${artifactId}`;
// ------------------------------------------------------------------
// WARM: the journey a user actually takes — gallery, then a click.
// ------------------------------------------------------------------
yield* browser.session(identity, async ({ page, step }) => {
await step("Open the Artifacts gallery", async () => {
await startSampling(page);
await visit(page, `${target.baseUrl}/artifacts`);
await page
.getByRole("link", { name: `Open artifact ${title}` })
.waitFor({ timeout: 30_000 });
// Discard everything from the gallery's own load: this scenario is
// about the OPEN, and the list has a loading state of its own that is
// out of scope here.
await page.evaluate(() => {
globalThis.__loadingSamples = [];
});
});
await step("Click through to the artifact", async () => {
await page.getByRole("link", { name: `Open artifact ${title}` }).click();
await artifactContent(page).getByTestId("artifact-marker").waitFor({ timeout: 60_000 });
});
await step("The whole open was one surface", async () => {
expectSingleLoadingSurface(await readSamples(page), "warm open");
});
await step("The skeleton hands over and gets out of the way", async () => {
// The cover is removed once the artifact has painted — not left over
// it at zero opacity, which would swallow every click.
await expect
.poll(async () => await page.locator('[data-testid="artifact-stage-cover"]').count(), {
timeout: 20_000,
message: "the loading cover was removed after the artifact rendered",
})
.toBe(0);
});
await step("Fill-mode sizing survived the handoff", async () => {
// The frame is the viewport below the title bar, exactly as it is in
// the steady state — so nothing about holding a skeleton over it
// changed the height chain it measures against.
const frameBox = await page.locator('[data-testid="artifact-shell-frame"]').boundingBox();
const viewport = page.viewportSize();
expect(frameBox?.height ?? 0, "the frame fills the room it was given").toBeGreaterThan(
200,
);
expect(
frameBox?.height ?? 0,
"the frame is bounded by the window, not by its content",
).toBeLessThanOrEqual(viewport?.height ?? 0);
const pageScrolls = await page.evaluate(
() => document.documentElement.scrollHeight > document.documentElement.clientHeight + 1,
);
expect(pageScrolls, "the console page still does not scroll").toBe(false);
});
});
// ------------------------------------------------------------------
// COLD: the deep link, in a context that has never loaded the console.
//
// The harder case, and the one the preload exists for: nothing is cached,
// so the row fetch and the renderer chunk both start from zero. This is
// where the three states used to be most visible, and where a regression
// in the preload would show up first.
// ------------------------------------------------------------------
yield* browser.session(identity, async ({ page, step }) => {
await step("Open the artifact by URL, cold", async () => {
await startSampling(page);
await page.goto(`${target.baseUrl}${detailPath}`, { waitUntil: "commit" });
await artifactContent(page).getByTestId("artifact-marker").waitFor({ timeout: 60_000 });
});
await step("The cold open was one surface too", async () => {
expectSingleLoadingSurface(await readSamples(page), "cold open");
});
await step("The artifact is live and interactive", async () => {
const rendered = artifactContent(page).getByTestId("artifact-marker");
expect(await rendered.textContent()).toContain(marker);
// Interactive, not merely painted: the cover is gone, so a scroll
// inside the artifact actually reaches it.
const scrolled = await artifactContent(page)
.getByTestId("artifact-scroll")
.evaluate((el) => {
el.scrollTop = 200;
return el.scrollTop;
});
expect(scrolled, "the artifact takes input after the handoff").toBeGreaterThan(0);
});
});
}).pipe(
Effect.ensuring(
Effect.suspend(() =>
artifactId === undefined
? Effect.void
: client.artifacts.remove({ params: { artifactId } }),
).pipe(Effect.ignore),
),
);
}),
);