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
160 changes: 160 additions & 0 deletions packages/experiments-realm/sample-command-card.gts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { action } from '@ember/object';
import { on } from '@ember/modifier';
import { tracked } from '@glimmer/tracking';

import {
CardDef,
field,
contains,
Component,
} from 'https://cardstack.com/base/card-api';
import StringField from 'https://cardstack.com/base/string';

import { Command, type CommandContext } from '@cardstack/runtime-common';
import SaveCardCommand from '@cardstack/boxel-host/commands/save-card';
import { getDefaultWritableRealmURL } from '@cardstack/boxel-host/test-helpers';

// ---------------------------------------------------------------------------
// Input / Output cards for SampleCommand

class GreetInput extends CardDef {
static displayName = 'Greet Input';
@field name = contains(StringField);
}

export class GreetOutput extends CardDef {
static displayName = 'Greet Output';
@field message = contains(StringField);
}

// ---------------------------------------------------------------------------
// SampleCommand — greets the given name, falling back to "World"

export class SampleCommand extends Command<
typeof GreetInput,
typeof GreetOutput
> {
static actionVerb = 'Greet';
name = 'SampleCommand';
description = 'Returns a greeting for the given name.';

async getInputType() {
return GreetInput;
}

protected async run(input: GreetInput): Promise<GreetOutput> {
let name = input?.name?.trim() || 'World';
return new GreetOutput({ message: `Hello, ${name}!` });
}
}

// ---------------------------------------------------------------------------
// SampleCommandCard

export class SampleCommandCard extends CardDef {
static displayName = 'Sample Command Card';

@field name = contains(StringField);

static isolated = class Isolated extends Component<typeof SampleCommandCard> {
@tracked commandOutput: string | null = null;
@tracked isRunning = false;
@tracked commandError: string | null = null;
@tracked savedCardId: string | null = null;
@tracked isSaving = false;

@action
async runSampleCommand() {
let commandContext = this.args.context?.commandContext as
| CommandContext
| undefined;
if (!commandContext) {
this.commandError = 'No commandContext available';
return;
}
this.isRunning = true;
this.commandOutput = null;
this.commandError = null;
try {
let cmd = new SampleCommand(commandContext);
let result = await cmd.execute({ name: this.args.model.name });
this.commandOutput = result?.message ?? null;
} catch (e: unknown) {
this.commandError = e instanceof Error ? e.message : String(e);
} finally {
this.isRunning = false;
}
}

@action
async runSampleSave() {
let commandContext = this.args.context?.commandContext as
| CommandContext
| undefined;
if (!commandContext) {
this.commandError = 'No commandContext available';
return;
}
this.isSaving = true;
this.savedCardId = null;
this.commandError = null;
try {
let newCard = new GreetOutput({
message: `Hello, ${this.args.model.name || 'World'}!`,
});
let cmd = new SaveCardCommand(commandContext);
await cmd.execute({
card: newCard,
realm: getDefaultWritableRealmURL(),
});
this.savedCardId = newCard.id ?? null;
} catch (e: unknown) {
this.commandError = e instanceof Error ? e.message : String(e);
} finally {
this.isSaving = false;
}
}

<template>
<div data-test-sample-command-card>
<p data-test-name>Name: {{@model.name}}</p>

<button
data-test-run-button
{{on 'click' this.runSampleCommand}}
disabled={{this.isRunning}}
type='button'
>
{{if this.isRunning 'Running...' 'Run Sample Command'}}
</button>

<button
data-test-save-button
{{on 'click' this.runSampleSave}}
disabled={{this.isSaving}}
type='button'
>
{{if this.isSaving 'Saving...' 'Run Sample Save'}}
</button>

{{#if this.commandOutput}}
<p data-test-output>{{this.commandOutput}}</p>
{{/if}}

{{#if this.savedCardId}}
<p data-test-saved-id>{{this.savedCardId}}</p>
{{/if}}

{{#if this.commandError}}
<p data-test-error>{{this.commandError}}</p>
{{/if}}
</div>
</template>
};

static embedded = class Embedded extends Component<typeof SampleCommandCard> {
<template>
<span data-test-sample-command-card-embedded>{{@model.name}}</span>
</template>
};
}
118 changes: 118 additions & 0 deletions packages/experiments-realm/sample-command-card.test.gts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { module, test } from 'qunit';
import { click, waitFor } from '@ember/test-helpers';
import {
renderCard,
visitOperatorMode,
fetchCard,
setupTestRealm,
} from '@cardstack/boxel-host/test-helpers';
import { SampleCommand, SampleCommandCard } from './sample-command-card';

// ---------------------------------------------------------------------------
// SampleCommand unit-style tests
// ---------------------------------------------------------------------------

test('SampleCommand returns a greeting for the given name', async (assert) => {
let cmd = new SampleCommand({} as any);
let result = await cmd.execute({ name: 'Alice' });
assert.strictEqual(result?.message, 'Hello, Alice!');
});

test('SampleCommand falls back to "World" when name is empty', async (assert) => {
let cmd = new SampleCommand({} as any);
let result = await cmd.execute({ name: '' });
assert.strictEqual(result?.message, 'Hello, World!');
});

test('SampleCommand falls back to "World" when name is whitespace-only', async (assert) => {
let cmd = new SampleCommand({} as any);
let result = await cmd.execute({ name: ' ' });
assert.strictEqual(result?.message, 'Hello, World!');
});

// ---------------------------------------------------------------------------
// SampleCommandCard field tests
// ---------------------------------------------------------------------------

test('SampleCommandCard can be instantiated with a name field', async (assert) => {
let card = new SampleCommandCard({ name: 'Bob' });
assert.strictEqual(card.name, 'Bob');
});

test('SampleCommandCard has a displayName', async (assert) => {
assert.strictEqual(SampleCommandCard.displayName, 'Sample Command Card');
});

// ---------------------------------------------------------------------------
// DOM assertion example
// ---------------------------------------------------------------------------

test('assert.dom — greeting renders correct text', async (assert) => {
let el = document.createElement('div');
el.innerHTML = `<p class="greeting">Hello, World!</p>`;
document.body.appendChild(el);

assert.dom('.greeting', el).exists();
assert.dom('.greeting', el).hasText('Hello, World!');
assert.dom('.greeting', el).hasTagName('p');

el.remove();
});

// ---------------------------------------------------------------------------
// Acceptance-style test: renders the live app in the iframe
// ---------------------------------------------------------------------------

test('SampleCommandCard renders name in isolated view', async (assert) => {
let card = new SampleCommandCard({ name: 'Alice' });

await renderCard(card);

assert.dom('[data-test-sample-command-card]').exists();
assert.dom('[data-test-name]').hasText('Name: Alice');
assert.dom('[data-test-run-button]').exists();
});

// ---------------------------------------------------------------------------
// Command execution test: render card and run a command via button click
// ---------------------------------------------------------------------------

test('SampleCommand runs and shows output when button is clicked', async (assert) => {
let card = new SampleCommandCard({ name: 'Alice' });

await renderCard(card);

assert.dom('[data-test-run-button]').exists();

await click('[data-test-run-button]');

assert.dom('[data-test-output]').hasText('Hello, Alice!');
});

// ---------------------------------------------------------------------------
// Save test: uses an in-memory test realm so nothing is written to the live realm
// ---------------------------------------------------------------------------

module('SampleCommandCard | save', function (hooks) {
setupTestRealm(hooks, { contents: {}, realmURL: 'http://test-realm/' });

test('runSampleSave saves a new card to the realm', async (assert) => {
let card = new SampleCommandCard({ name: 'Alice' });

await renderCard(card);

assert.dom('[data-test-save-button]').exists();

await click('[data-test-save-button]');
await waitFor('[data-test-saved-id]');

assert.dom('[data-test-error]').doesNotExist();
assert.dom('[data-test-saved-id]').exists('saved card id is shown');

let savedId = document
.querySelector('[data-test-saved-id]')!
.textContent!.trim();
let saved = await fetchCard(savedId);
assert.strictEqual(saved.data.attributes.message, 'Hello, Alice!');
});
});
2 changes: 2 additions & 0 deletions packages/experiments-realm/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
"experimentalDecorators": true,
"paths": {
"https://cardstack.com/base/*": ["../base/*"],
"@cardstack/boxel-host/commands/*": ["../host/app/commands/*"],
"@cardstack/boxel-host/test-helpers": ["../host/app/test-helpers/index.ts"],
"@cardstack/catalog/commands/*": ["../catalog-realm/commands/*"],
"@cardstack/catalog/*": ["../catalog-realm/catalog-app/*"]
},
Expand Down
Loading
Loading