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
36 changes: 26 additions & 10 deletions typescript/packages/jumble/integration/basic-flow.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Browser, launch, Page } from "@astral/astral";
import { assert } from "@std/assert";
import {
afterAll,
afterEach,
Expand All @@ -13,8 +12,10 @@ import {
inspectCharm,
login,
sleep,
snapshot,
waitForSelectorWithText,
} from "./utils.ts";
import { assert } from "@std/assert";

const TOOLSHED_API_URL = Deno.env.get("TOOLSHED_API_URL") ??
"http://localhost:8000/";
Expand Down Expand Up @@ -48,23 +49,32 @@ describe("integration", () => {
});

it("renders a new charm", async () => {
assert(page);
assert(testCharm);
assert(page, "Page should be defined");
assert(testCharm, "Test charm should be defined");

await snapshot(page, "Initial state");

const anchor = await page.waitForSelector("nav a");
assert(
(await anchor.innerText()) === "common-knowledge",
"Logged in and Common Knowledge title renders",
);

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

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

console.log("Clicking button");
// Sometimes clicking this button throws:
Expand All @@ -76,29 +86,35 @@ describe("integration", () => {
"div[aria-label='charm-content'] button",
);
await button.click();
await snapshot(page, "Button clicked");

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

await snapshot(page, "Title changed");

console.log("Inspecting charm to verify updates propagated from browser.");
const charm = await inspectCharm(
TOOLSHED_API_URL,
testCharm.space,
testCharm.charmId,
);

console.log("Charm:", charm);
assert(charm.includes("Simple Value: 2"), "Charm updates propagated.");
assert(
charm.includes("Simple Value: 2"),
"Charm updates propagated.",
);
});

// Placeholder test ensuring browser can be used
// across multiple tests (replace when we have more integration tests!)
it("[placeholder]", () => {
assert(page);
assert(testCharm);
assert(page, "Page should be defined");
assert(testCharm, "Test charm should be defined");
});
});
62 changes: 47 additions & 15 deletions typescript/packages/jumble/integration/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { ElementHandle, Page } from "@astral/astral";
import * as path from "@std/path";
import { assert } from "@std/assert";
import { ensureDirSync } from "@std/fs";
import { join } from "@std/path";

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

Expand All @@ -8,9 +11,40 @@ export const decode = (() => {
return (buffer: Uint8Array): string => decoder.decode(buffer);
})();

const RECORD_SNAPSHOTS = false;
const SNAPSHOTS_DIR = join(Deno.cwd(), "test_snapshots");
console.log("SNAPSHOTS_DIR=", SNAPSHOTS_DIR);

export async function snapshot(page: Page | undefined, snapshotName: string) {
console.log(snapshotName);
if (RECORD_SNAPSHOTS && page && snapshotName) {
ensureDirSync(SNAPSHOTS_DIR);
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const filePrefix = `${snapshotName}_${timestamp}`;

const screenshot = await page.screenshot();
Deno.writeFileSync(`${SNAPSHOTS_DIR}/${filePrefix}.png`, screenshot);

const html = await page.content();
Deno.writeTextFileSync(`${SNAPSHOTS_DIR}/${filePrefix}.html`, html);

console.log(`→ Snapshot saved: ${filePrefix}`);
}
}

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

export async function tryClick(
el?: ElementHandle | null,
page?: Page,
): Promise<void> {
await snapshot(page, "try_click_element");
assert(el, "Element does not exist or is not clickable");

await el.click();
}

export const login = async (page: Page) => {
// Wait a second :(
// See if #user-avatar is rendered
Expand All @@ -24,44 +58,42 @@ export const login = async (page: Page) => {

// If not logged in, see if any credential data is
// persisting. If so, destroy local data.
let buttons = await page.$$("button");
for (const button of buttons) {
if ((await button.innerText()) === "Clear Saved Credentials") {
await button.click();
}
let button = await page.$("button[aria-label='clear-credentials']");
if (button) {
await tryClick(button, page);
}

// Try log in
console.log("Logging in");

// Click the first button, "register"
let button = await page.$("button");
await button!.click();
button = await page.$("button[aria-label='register']");
await tryClick(button, page);

// Click the first button, "register with passphrase"
button = await page.$("button");
await button!.click();
button = await page.$("button[aria-label='register-with-passphrase']");
await tryClick(button, page);

// Get the mnemonic from textarea.
let input = await page.$("textarea");
let input = await page.$("textarea[aria-label='mnemonic']");
const mnemonic = await input!.evaluate((textarea: HTMLInputElement) =>
textarea.value
);

// Click the SECOND button, "continue to login"
buttons = await page.$$("button");
await buttons[1]!.click();
button = await page.$("button[aria-label='continue-login']");
await tryClick(button, page);

// Paste the mnemonic in the input.
input = await page.$("input");
input = await page.$("input[aria-label='enter-passphrase']");
await input!.evaluate(
(input: HTMLInputElement, mnemonic: string) => input.value = mnemonic,
{ args: [mnemonic] },
);

// Click the only button, "login"
button = await page.$("button");
await button!.click();
button = await page.$("button[aria-label='login']");
await tryClick(button, page);
};

export const waitForSelectorWithText = async (
Expand Down
26 changes: 24 additions & 2 deletions typescript/packages/jumble/src/views/AuthenticationView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,15 @@ function SuccessRegistration({
<p className="mb-2">Your Secret Recovery Phrase:</p>
<div className="relative">
<textarea
aria-label="mnemonic"
readOnly
value={mnemonic}
rows={3}
className="w-full p-2 pr-10 border-2 border-black resize-none"
/>
<button
type="button"
aria-label="copy-mnemonic"
onClick={copyToClipboard}
className={`absolute right-2 top-1/2 transform -translate-y-1/2 ${
copied ? "text-green-500" : ""
Expand All @@ -115,7 +117,12 @@ function SuccessRegistration({
</div>
)
)}
<button type="button" className={BTN_PRIMARY} onClick={onLogin}>
<button
type="button"
className={BTN_PRIMARY}
onClick={onLogin}
aria-label="continue-login"
>
<LuLock className="w-5 h-5" /> Continue to Login
</button>
</div>
Expand Down Expand Up @@ -292,6 +299,7 @@ export function AuthenticationView() {
<button
type="button"
className={BTN_PRIMARY}
aria-label="unlock-with-last-key"
onClick={async () => {
if (storedCredential.method === AUTH_METHOD_PASSKEY) {
await handleLogin(AUTH_METHOD_PASSKEY);
Expand All @@ -308,13 +316,15 @@ export function AuthenticationView() {
</button>
<button
type="button"
aria-label="login-new-method"
className={LIST_ITEM}
onClick={() => setFlow("login")}
>
<LuLock className="w-5 h-5" /> Login with Different Method
</button>
<button
type="button"
aria-label="register"
className={LIST_ITEM}
onClick={() => setFlow("register")}
>
Expand All @@ -323,6 +333,7 @@ export function AuthenticationView() {
<button
type="button"
className={LIST_ITEM}
aria-label="clear-credentials"
onClick={() => {
clearStoredCredential();
setStoredCredential(null);
Expand All @@ -337,13 +348,15 @@ export function AuthenticationView() {
<button
type="button"
className={BTN_PRIMARY}
aria-label="register"
onClick={() => setFlow("register")}
>
<LuCirclePlus className="w-5 h-5" /> Register
</button>
<button
type="button"
className={BTN_PRIMARY}
aria-label="login"
onClick={() => setFlow("login")}
>
<LuLock className="w-5 h-5" /> Login
Expand All @@ -363,6 +376,7 @@ export function AuthenticationView() {
key={m}
type="button"
className={LIST_ITEM}
aria-label={"method-" + m}
onClick={async () => await handleMethodSelect(m)}
>
{m === AUTH_METHOD_PASSKEY
Expand All @@ -381,6 +395,7 @@ export function AuthenticationView() {
<button
type="button"
className={BTN_PRIMARY}
aria-label="back"
onClick={() => setFlow(null)}
>
<LuArrowLeft className="w-5 h-5" /> Back
Expand All @@ -395,6 +410,7 @@ export function AuthenticationView() {
<button
type="button"
className={BTN_PRIMARY}
aria-label="register-with-passphrase"
onClick={() => handleRegister(AUTH_METHOD_PASSPHRASE)}
>
<LuKeyRound className="w-5 h-5" /> Register with Passphrase
Expand All @@ -418,16 +434,22 @@ export function AuthenticationView() {
name={AUTH_METHOD_PASSPHRASE}
className="w-full p-2 pr-10 border-2 border-black"
placeholder="Enter your passphrase"
aria-label="enter-passphrase"
autoComplete="current-password"
/>
<button type="submit" className={BTN_PRIMARY}>
<button
type="submit"
className={BTN_PRIMARY}
aria-label="login"
>
<LuLock className="w-5 h-5" /> Login
</button>
</form>
)}
<button
type="button"
className={BTN_PRIMARY}
aria-label="back"
onClick={() => setFlow(null)}
>
<LuArrowLeft className="w-5 h-5" /> Back
Expand Down