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
2 changes: 1 addition & 1 deletion .github/workflows/deno.yml
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,7 @@ jobs:
host: ${{ secrets.BASTION_HOST }}
username: bastion
key: ${{ secrets.BASTION_SSH_PRIVATE_KEY }}
command_timeout: 10m
command_timeout: 20m
script: |
/opt/ct/run-pattern-tests-against-toolshed.sh ${{ github.sha }} \
https://toolshed.saga-castor.ts.net \
Expand Down
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,5 @@ If you are developing runtime code, read the following documentation:
practices
- `docs/common/UI_TESTING.md` - How to work with shadow dom in our integration
tests
- `docs/common/LOCAL_DEV_SERVERS.md` - **CRITICAL**: How to start local dev
servers correctly (use `dev-local` for shell, not `dev`)
8 changes: 4 additions & 4 deletions packages/charm/src/ops/charms-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@ export class CharmsController<T = unknown> {
return this.#manager;
}

async create(
async create<U = T>(
program: RuntimeProgram | string,
options: CreateCharmOptions = {},
cause: string | undefined = undefined,
): Promise<CharmController<T>> {
): Promise<CharmController<U>> {
this.disposeCheck();
const recipe = await compileProgram(this.#manager, program);
const charm = await this.#manager.runPersistent<T>(
const charm = await this.#manager.runPersistent<U>(
recipe,
options.input,
cause,
Expand All @@ -45,7 +45,7 @@ export class CharmsController<T = unknown> {
);
await this.#manager.runtime.idle();
await this.#manager.synced();
return new CharmController<T>(this.#manager, charm);
return new CharmController<U>(this.#manager, charm);
}

async get<S extends JSONSchema = JSONSchema>(
Expand Down
13 changes: 12 additions & 1 deletion packages/integration/shell-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,20 @@ export async function login(page: Page, identity: Identity): Promise<void> {
);
}

export interface ShellIntegrationConfig {
pipeConsole?: boolean;
}

export class ShellIntegration {
#browser?: Browser;
#page?: Page;
#exceptions: Array<string> = [];
#errorLogs: Array<string> = [];
#config: ShellIntegrationConfig;

constructor(config: ShellIntegrationConfig = {}) {
this.#config = config;
}

bindLifecycle() {
beforeAll(this.#beforeAll);
Expand Down Expand Up @@ -161,7 +170,9 @@ export class ShellIntegration {
if (e.detail.type === "error") {
this.#errorLogs.push(e.detail.text);
}
pipeConsole(e);
if (this.#config.pipeConsole) {
pipeConsole(e);
}
});
this.#page.addEventListener("dialog", dismissDialogs);
this.#page.addEventListener("pageerror", (e: PageErrorEvent) => {
Expand Down
50 changes: 24 additions & 26 deletions packages/patterns/integration/counter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { CharmController, CharmsController } from "@commontools/charm/ops";
import { ShellIntegration } from "@commontools/integration/shell-utils";
import { afterAll, beforeAll, describe, it } from "@std/testing/bdd";
import { join } from "@std/path";
import { assert, assertEquals } from "@std/assert";
import { assertEquals } from "@std/assert";
import { Identity } from "@commontools/identity";
import { FileSystemProgramResolver } from "@commontools/js-compiler";

Expand Down Expand Up @@ -50,32 +50,34 @@ describe("counter direct operations test", () => {
identity,
});

const counterResult = await page.waitForSelector("#counter-result", {
strategy: "pierce",
});
assert(counterResult, "Should find counter-result element");

// Verify initial value is 0
const initialText = await counterResult.evaluate((el: HTMLElement) =>
el.textContent
);
assertEquals(initialText?.trim(), "Counter is the 0th number");
await waitFor(async () => {
const counterResult = await page.waitForSelector("#counter-result", {
strategy: "pierce",
});
const initialText = await counterResult.evaluate((el: HTMLElement) =>
el.textContent
);
return initialText?.trim() === "Counter is the 0th number";
});

assertEquals(charm.result.get(["value"]), 0);
});

it("should update counter value via direct operation (live)", async () => {
const page = shell.page();

// Get the counter result element
const counterResult = await page.waitForSelector("#counter-result", {
await page.waitForSelector("#counter-result", {
strategy: "pierce",
});

console.log("Setting counter value to 42 via direct operation");
await charm.result.set(42, ["value"]);

await waitFor(async () => {
const counterResult = await page.waitForSelector("#counter-result", {
strategy: "pierce",
});

const updatedText = await counterResult.evaluate((el: HTMLElement) =>
el.textContent
);
Expand Down Expand Up @@ -105,19 +107,15 @@ describe("counter direct operations test", () => {
});

// Get the counter result element after refresh
const counterResult = await page.waitForSelector("#counter-result", {
strategy: "pierce",
});
assert(counterResult, "Should find counter-result element after refresh");
await waitFor(async () => {
const counterResult = await page.waitForSelector("#counter-result", {
strategy: "pierce",
});

// Check if the UI shows the updated value after refresh
const textAfterRefresh = await counterResult.evaluate((el: HTMLElement) =>
el.textContent
);
assertEquals(
textAfterRefresh?.trim(),
"Counter is the 42th number",
"UI should show persisted value after refresh",
);
const textAfterRefresh = await counterResult.evaluate((el: HTMLElement) =>
el.textContent
);
return textAfterRefresh?.trim() === "Counter is the 42th number";
});
});
});
81 changes: 46 additions & 35 deletions packages/patterns/integration/ct-checkbox.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { env } from "@commontools/integration";
import { sleep } from "@commontools/utils/sleep";
import { env, Page, waitFor } from "@commontools/integration";
import { ShellIntegration } from "@commontools/integration/shell-utils";
import { afterAll, beforeAll, describe, it } from "@std/testing/bdd";
import { join } from "@std/path";
import { assertEquals } from "@std/assert";
import { Identity } from "@commontools/identity";
import { CharmsController } from "@commontools/charm/ops";
import { ANYONE_USER } from "@commontools/memory/acl";
Expand Down Expand Up @@ -66,50 +64,63 @@ testComponents.forEach(({ name, file }) => {

it("should show disabled content initially", async () => {
const page = shell.page();

const featureStatus = await page.waitForSelector("#feature-status", {
strategy: "pierce",
await waitFor(async () => {
const statusText = await getFeatureStatus(page);
return statusText === "⚠ Feature is disabled";
});
const statusText = await featureStatus.evaluate((el: HTMLElement) =>
el.textContent
);
assertEquals(statusText?.trim(), "⚠ Feature is disabled");
});

it("should toggle to enabled content when checkbox is clicked", async () => {
const page = shell.page();

const checkbox = await page.waitForSelector("ct-checkbox", {
strategy: "pierce",
});
await checkbox.click();
await sleep(500);

const featureStatus = await page.$("#feature-status", {
strategy: "pierce",
await clickCtCheckbox(page);
await waitFor(async () => {
const statusText = await getFeatureStatus(page);
return statusText === "✓ Feature is enabled!";
});
const statusText = await featureStatus?.evaluate((el: HTMLElement) =>
el.textContent
);
assertEquals(statusText?.trim(), "✓ Feature is enabled!");
});

it("should toggle back to disabled content when checkbox is clicked again", async () => {
const page = shell.page();

const checkbox = await page.$("ct-checkbox", {
strategy: "pierce",
});
await checkbox?.click();
await sleep(1000);

const featureStatus = await page.$("#feature-status", {
strategy: "pierce",
await clickCtCheckbox(page);
await waitFor(async () => {
const statusText = await getFeatureStatus(page);
return statusText === "⚠ Feature is disabled";
});
const statusText = await featureStatus?.evaluate((el: HTMLElement) =>
el.textContent
);
assertEquals(statusText?.trim(), "⚠ Feature is disabled");
});
});
});

function clickCtCheckbox(page: Page) {
return waitFor(async () => {
const checkbox = await page.waitForSelector("ct-checkbox", {
strategy: "pierce",
});
// This could throw due to lacking a box model to click on.
// Catch in lieu of handling time sensitivity.
try {
await checkbox.click();
return true;
} catch (_) {
return false;
}
});
}

async function getFeatureStatus(
page: Page,
): Promise<string | undefined | null> {
const featureStatus = await page.waitForSelector("#feature-status", {
strategy: "pierce",
});
// This could throw due to lacking a box model to click on.
// Catch in lieu of handling time sensitivity.
try {
const statusText = await featureStatus.evaluate((el: HTMLElement) =>
el.textContent
);
return statusText?.trim();
} catch (_) {
return null;
}
}
Loading
Loading