Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions apps/web/ct/harness/stubs/use-content-backend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* CT stub for `sections-editor/use-content-backend`.
*
* The real hook picks a project's content backend from the org flag, the
* project's `deco serve` connection and a probe of its schema, all through
* `useProjectContext()`, which this harness doesn't mount. Field specs cover
* the editor as v7 sites see it, so every project reads as legacy here.
*/
import type { ContentBackend } from "@/components/sections-editor/content-backend";

export function useContentBackend(
_virtualMcpId: string | null | undefined,
_branch: string | null | undefined,
): ContentBackend {
return { kind: "legacy" };
}
7 changes: 7 additions & 0 deletions apps/web/playwright-ct.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,13 @@ export default defineConfig({
find: /^@\/hooks\/use-new-blocks-editor$/,
replacement: stub("use-new-blocks-editor.ts"),
},
// The content backend reads the org flag and project via
// useProjectContext(); fields are tested as v7 (legacy) sites see them.
// Matches the fields' relative `../use-content-backend` import too.
{
find: /^.*\/use-content-backend$/,
replacement: stub("use-content-backend.ts"),
},
// Editor uploads PUT to the org filesystem via useProjectContext(), which a bare mount lacks.
{
find: /^@\/components\/markdown-editor\/use-file-upload$/,
Expand Down
125 changes: 125 additions & 0 deletions apps/web/src/components/sections-editor/use-content-backend.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { describe, expect, test } from "bun:test";
import {
type BatchCall,
type BatchOutcome,
type ContentClient,
PROTOCOL_NAME,
PROTOCOL_VERSION,
} from "@decocms/blocks/protocol";
import { selectContentBackend } from "./content-backend";
import { probe, probeSandbox } from "./use-content-backend";

const deco1 = {
manifest: { blocks: {} },
schema: { definitions: {}, root: {} },
};

/** The GitHub backend answering `describe` + `schema.get` with `schema`. */
function githubServing(schema: unknown): ContentClient {
return {
batch: async (calls: BatchCall[]) =>
calls.map(
(call): BatchOutcome =>
call.method === "describe"
? {
ok: true,
result: { protocol: PROTOCOL_NAME, version: PROTOCOL_VERSION },
}
: {
ok: true,
result: {
notModified: false,
version: "s1",
resolvedRef: "main",
schema,
},
},
),
} as unknown as ContentClient;
}

/** What `useContentBackend` decides for a flag-on Fast Preview session. */
async function decideWithFlagOn(schema: unknown) {
const probed = await probe(githubServing(schema));
return selectContentBackend({
hasProject: true,
flagEnabled: true,
hasServeConnection: false,
hasLocalTunnel: false,
runtime: "cms",
githubSite: probed.v8Schema ? "v8" : "v7",
});
}

describe("v7/v8 detection over the GitHub backend", () => {
test('a schema with "blocksMajor": 8 is a v8 site', async () => {
expect(await decideWithFlagOn({ major: 1, blocksMajor: 8, ...deco1 })).toBe(
"protocol-github",
);
});

// The server serves `schema.gen.json`, else a v7 `meta.gen.json`, as
// written: the file name never makes a site v8, only the field does.
test("a committed schema without blocksMajor stays v7 with the flag on", async () => {
// A v7 `meta.gen.json`, and a `schema.gen.json` from an older `deco schema`.
expect(await decideWithFlagOn(deco1)).toBe("legacy");
expect(await decideWithFlagOn({ major: 1, ...deco1 })).toBe("legacy");
});

test("any other blocksMajor stays v7", async () => {
for (const blocksMajor of [7, "8", null, 9]) {
expect(await decideWithFlagOn({ major: 1, blocksMajor, ...deco1 })).toBe(
"legacy",
);
}
});

test("no committed schema stays v7", async () => {
expect(await decideWithFlagOn(null)).toBe("legacy");
});
});

describe("v7/v8 detection over a sandbox's daemon", () => {
async function decide(schema: unknown) {
const probed = await probe(githubServing(schema));
return selectContentBackend({
hasProject: true,
flagEnabled: true,
hasServeConnection: false,
hasLocalTunnel: false,
runtime: "sandbox",
githubSite: "loading",
sandboxSite: probed.v8Schema ? "v8" : "v7",
});
}

test('a working tree whose schema says "blocksMajor": 8 is a v8 site', async () => {
expect(await decide({ major: 1, blocksMajor: 8, ...deco1 })).toBe(
"protocol-sandbox",
);
});

test("a v7 working tree (or none) stays legacy", async () => {
expect(await decide({ major: 1, ...deco1 })).toBe("legacy");
expect(await decide(null)).toBe("legacy");
});
});

describe("sandbox probe", () => {
const endpoint = "http://studio.test/rpc";

test("an older daemon answering 404 is v7, not an error", async () => {
expect(
await probeSandbox(
endpoint,
async () => new Response("404 page not found", { status: 404 }),
),
).toBe(null);
});

test("any other failure is an error (retried)", async () => {
await expect(
probeSandbox(endpoint, async () => new Response("boom", { status: 502 })),
).rejects.toThrow();
});
});
Loading
Loading