Skip to content
Merged
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
3 changes: 3 additions & 0 deletions typescript/packages/common-charm/src/charm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,9 @@ export class CharmManager {
const recipe = recipeId ? getRecipe(recipeId) : undefined;

if (!recipe || charm.get() === undefined) {
console.warn("recipeId", recipeId);
console.warn("recipe", recipe);
console.warn("charm", charm.get());
console.warn(`Not a charm: ${JSON.stringify(getEntityId(charm))}`);
}

Expand Down
43 changes: 36 additions & 7 deletions typescript/packages/common-cli/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import {
import { getEntityId, isStream } from "@commontools/runner";
import { Identity } from "@commontools/identity";

const { space, charmId, recipeFile, cause } = parseArgs(Deno.args, {
const { space, charmId, recipeFile, cause, quit } = parseArgs(Deno.args, {
string: ["space", "charmId", "recipeFile", "cause"],
default: {},
boolean: ["quit"],
default: { quit: false },
});

const toolshedUrl = Deno.env.get("TOOLSHED_API_URL") ??
Expand All @@ -22,22 +23,42 @@ setBobbyServerUrl(toolshedUrl);

async function main() {
const identity = await Identity.fromPassphrase("common-cli");
console.log("params:", { space, identity, charmId, recipeFile, cause });
console.log("params:", {
space,
identity,
charmId,
recipeFile,
cause,
quit,
toolshedUrl,
});
const manager = await CharmManager.open({
space: (space as `did:key:${string}`) ?? identity.did(),
signer: identity,
});
const charms = await manager.getCharms();

const charms = manager.getCharms();
charms.sink((charms) => {
console.log(
"charms:",
charms.map((c) => c.toJSON().cell?.["/"]),
"all charms:",
charms.map((c) => getEntityId(c)?.["/"]),
);
});

if (charmId) {
const charm = await manager.get(charmId);
if (quit) {
if (!charm) {
console.error("charm not found:", charmId);
Deno.exit(1);
}
console.log("charm:", charmId);
console.log("charm:", JSON.stringify(charm.get(), null, 2));
console.log(
"sourceCell:",
JSON.stringify(charm.getSourceCell().get(), null, 2),
);
Deno.exit(0);
}
charm?.sink((value) => {
console.log("charm:", charmId, value);
});
Expand All @@ -59,8 +80,16 @@ async function main() {
console.log("running updater");
updater.send({ newValues: ["test"] });
}
if (quit) {
await storage.synced();
Deno.exit(0);
}
} catch (error) {
console.error("Error loading and compiling recipe:", error);
if (quit) {
await storage.synced();
Deno.exit(1);
}
}
}

Expand Down
3 changes: 2 additions & 1 deletion typescript/packages/common-cli/recipes/simpleValue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
recipe,
schema,
UI,
str,
} from "@commontools/builder";

const updaterSchema = {
Expand Down Expand Up @@ -64,7 +65,7 @@ export default recipe(inputSchema, outputSchema, ({ values }) => {
console.log("values#", values?.length);
});
return {
[NAME]: "Simple Value",
[NAME]: str`Simple Value: ${values.length}`,
[UI]: (
<div>
<button type="button" onClick={adder({ values })}>Add Value</button>
Expand Down
1 change: 1 addition & 0 deletions typescript/packages/deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

68 changes: 61 additions & 7 deletions typescript/packages/jumble/integration/smoke-test.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,82 @@
import { launch } from "@astral/astral";
import { Browser, launch } from "@astral/astral";
import { assert } from "@std/assert";
import { login } from "./utils.ts";
import {
addCharm,
inspectCharm,
login,
sleep,
waitForSelectorWithText,
} from "./utils.ts";

const FRONTEND_URL = "http://localhost:5173/";
const TOOLSHED_API_URL = Deno.env.get("TOOLSHED_API_URL") ??
"http://localhost:8000/";
const FRONTEND_URL = Deno.env.get("FRONTEND_URL") ?? "http://localhost:5173/";

async function main(browser: Browser) {
console.log(`TOOLSHED_API_URL=${TOOLSHED_API_URL}`);
console.log(`FRONTEND_URL=${FRONTEND_URL}`);

const { charmId, space } = await addCharm(TOOLSHED_API_URL);
console.log(`Charm added`, { charmId, space });

async function main() {
const browser = await launch();
console.log(`Waiting to open website at ${FRONTEND_URL}`);
const page = await browser.newPage(FRONTEND_URL);
console.log(`Opened website at ${FRONTEND_URL}`);

console.log("Logging in");
await login(page);

console.log("Checking if logged in");
const anchor = await page.waitForSelector("nav a");
assert(
(await anchor.innerText()) === "common-knowledge",
"Logged in and Common Knowledge title renders",
);
await browser.close();

await page.goto(`${FRONTEND_URL}${space}/${charmId}`);
console.log(`Waiting for charm to render`);

await waitForSelectorWithText(
page,
"a[aria-current='charm-title']",
"Simple Value: 1",
);
console.log("Charm rendered.");

console.log("Clicking button");
// Sometimes clicking this button throws:
// https://jsr.io/@astral/astral/0.5.2/src/element_handle.ts#L192
// As if the reference was invalidated by a spurious re-render between
// getting an element handle, and clicking it.
await sleep(1000);
const button = await page.waitForSelector(
"div[aria-label='charm-content'] button",
);
await button.click();

console.log("Checking if title changed");
await waitForSelectorWithText(
page,
"a[aria-current='charm-title']",
"Simple Value: 2",
);
console.log("Title changed");

console.log("Inspecting charm to verify updates propagated from browser.");
const charm = await inspectCharm(TOOLSHED_API_URL, space, charmId);
console.log("Charm:", charm);
assert(charm.includes("Simple Value: 2"), "Charm updates propagated.");
}

let browser = null;
try {
await main();
browser = await launch();
await main(browser);
await browser.close();
} catch (e) {
if (browser) {
await browser.close();
}
console.error(e);
Deno.exit(1);
}
97 changes: 96 additions & 1 deletion typescript/packages/jumble/integration/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
import { Page } from "@astral/astral";
import { ElementHandle, Page } from "@astral/astral";
import * as path from "@std/path";

const COMMON_CLI_PATH = path.join(import.meta.dirname!, "../../common-cli");

export const decode = (() => {
const decoder = new TextDecoder();
return (buffer: Uint8Array): string => decoder.decode(buffer);
})();

export const sleep = (ms: number) =>
new Promise((resolve) => setTimeout(resolve, ms));

export const login = async (page: Page) => {
// First, see if any credential data is
Expand Down Expand Up @@ -39,3 +50,87 @@ export const login = async (page: Page) => {
button = await page.$("button");
await button!.click();
};

export const waitForSelectorWithText = async (
page: Page,
selector: string,
text: string,
): Promise<ElementHandle> => {
const retries = 30;
const timeout = 200;

for (let i = 0; i < retries; i++) {
const el = await page.$(selector);
if (!el) {
await sleep(timeout);
continue;
}
if ((await el.innerText()) === text) {
return el;
}
}
throw new Error(`Timed out waiting for "${selector}" to have text "${text}"`);
};

export const addCharm = async (toolshedUrl: string) => {
const space = `ci-${Date.now()}-${
Math.random().toString(36).substring(2, 15)
}`;
const { success, stderr } = await (new Deno.Command(Deno.execPath(), {
args: [
"task",
"start",
"--space",
space,
"--recipeFile",
"recipes/simpleValue.tsx",
"--cause",
"ci",
"--quit",
"true",
],
env: {
"TOOLSHED_API_URL": toolshedUrl,
},
cwd: COMMON_CLI_PATH,
})).output();

if (!success) {
throw new Error(`Failed to add charm: ${decode(stderr)}`);
}

return {
charmId: "baedreic5a2muxtlgvn6u36lmcp3tdoq5sih3nbachysw4srquvga5fjtem",
space,
};
};

export const inspectCharm = async (
toolshedUrl: string,
space: string,
charmId: string,
) => {
const { success, stdout, stderr } = await (new Deno.Command(Deno.execPath(), {
args: [
"task",
"start",
"--space",
space,
"--charmId",
charmId,
"--quit",
"true",
],
env: {
"TOOLSHED_API_URL": toolshedUrl,
},
cwd: COMMON_CLI_PATH,
})).output();

if (!success) {
console.log(decode(stdout));
throw new Error(`Failed to inspect charm: ${decode(stderr)}`);
}

return decode(stdout);
};
7 changes: 6 additions & 1 deletion typescript/packages/jumble/src/components/CharmRunner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,12 @@ function RawCharmRenderer({ charm, className = "" }: CharmRendererProps) {
</div>
)
: null}
<div className={className + " overflow-clip"} ref={containerRef}></div>
<div
className={className + " overflow-clip"}
ref={containerRef}
aria-label="charm-content"
>
</div>
</>
);
}
Expand Down
1 change: 1 addition & 0 deletions typescript/packages/jumble/src/components/NavPath.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export function NavPath({ replicaId, charmId }: NavPathProps) {
<NavLink
to={`/${replicaId}/${charmId}`}
className="text-gray-700 font-bold"
aria-current="charm-title"
>
{charmName}
</NavLink>
Expand Down