From ec1475913408e82cdd4fefaf26543b45fcb67224 Mon Sep 17 00:00:00 2001 From: Richard Tan Date: Thu, 5 Feb 2026 15:16:41 +0800 Subject: [PATCH 1/3] Remove catalog tests out of monorepo --- .github/workflows/ci.yaml | 2 +- .../host/scripts/test-wait-for-servers.sh | 3 +- .../tests/acceptance/catalog-app-test.gts | 2572 ----------------- .../acceptance/real-catalog-app-test.gts | 67 - .../tests/integration/audio-fields-test.gts | 484 ---- .../color-field-configuration-test.gts | 425 --- .../commands/listing-update-specs-test.gts | 185 -- .../commands/set-user-system-card-test.gts | 63 - .../commands/upload-image-test.gts | 389 --- .../integration/date-time-fields-test.gts | 1039 ------- .../image-field-configuration-test.gts | 270 -- ...ultiple-image-field-configuration-test.gts | 316 -- .../number-field-configuration-test.gts | 293 -- packages/realm-server/package.json | 2 +- ...iments.sh => start-all-except-optional.sh} | 2 +- .../scripts/start-services-for-host-tests.sh | 1 + 16 files changed, 5 insertions(+), 6108 deletions(-) delete mode 100644 packages/host/tests/acceptance/catalog-app-test.gts delete mode 100644 packages/host/tests/acceptance/real-catalog-app-test.gts delete mode 100644 packages/host/tests/integration/audio-fields-test.gts delete mode 100644 packages/host/tests/integration/color-field-configuration-test.gts delete mode 100644 packages/host/tests/integration/commands/listing-update-specs-test.gts delete mode 100644 packages/host/tests/integration/commands/set-user-system-card-test.gts delete mode 100644 packages/host/tests/integration/commands/upload-image-test.gts delete mode 100644 packages/host/tests/integration/date-time-fields-test.gts delete mode 100644 packages/host/tests/integration/image-field-configuration-test.gts delete mode 100644 packages/host/tests/integration/multiple-image-field-configuration-test.gts delete mode 100644 packages/host/tests/integration/number-field-configuration-test.gts rename packages/realm-server/scripts/{start-all-except-experiments.sh => start-all-except-optional.sh} (77%) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 64740bc19e7..c43f6f145be 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -549,7 +549,7 @@ jobs: wait-for: 3m wait-on: http-get://localhost:4200 - name: Start realm servers - run: pnpm start:skip-experiments | tee -a /tmp/server.log & + run: pnpm start:skip-optional-realms | tee -a /tmp/server.log & working-directory: packages/realm-server - name: create realm users run: pnpm register-realm-users diff --git a/packages/host/scripts/test-wait-for-servers.sh b/packages/host/scripts/test-wait-for-servers.sh index 9c469cccd7b..db8154cacd0 100755 --- a/packages/host/scripts/test-wait-for-servers.sh +++ b/packages/host/scripts/test-wait-for-servers.sh @@ -29,7 +29,6 @@ TEST_REALM="http-get://localhost:4202/test/" READY_PATH="_readiness-check?acceptHeader=application%2Fvnd.api%2Bjson" BASE_REALM_READY="$(to_wait_url "${BASE_REALM_URL}")${READY_PATH}" -CATALOG_REALM_READY="$(to_wait_url "${CATALOG_REALM_URL}")${READY_PATH}" NODE_TEST_REALM_READY="$NODE_TEST_REALM$READY_PATH" SKILLS_REALM_READY="$(to_wait_url "${SKILLS_REALM_URL}")${READY_PATH}" TEST_REALM_READY="$TEST_REALM$READY_PATH" @@ -39,5 +38,5 @@ SMTP_4_DEV_URL="http://localhost:5001" WAIT_ON_TIMEOUT=600000 NODE_NO_WARNINGS=1 start-server-and-test \ 'pnpm run wait' \ - "$BASE_REALM_READY|$CATALOG_REALM_READY|$NODE_TEST_REALM_READY|$SKILLS_REALM_READY|$TEST_REALM_READY|$SYNAPSE_URL|$SMTP_4_DEV_URL" \ + "$BASE_REALM_READY|$NODE_TEST_REALM_READY|$SKILLS_REALM_READY|$TEST_REALM_READY|$SYNAPSE_URL|$SMTP_4_DEV_URL" \ 'ember-test-pre-built' diff --git a/packages/host/tests/acceptance/catalog-app-test.gts b/packages/host/tests/acceptance/catalog-app-test.gts deleted file mode 100644 index 9ea064f1445..00000000000 --- a/packages/host/tests/acceptance/catalog-app-test.gts +++ /dev/null @@ -1,2572 +0,0 @@ -import { - click, - waitFor, - waitUntil, - fillIn, - settled, - triggerEvent, -} from '@ember/test-helpers'; - -import { getService } from '@universal-ember/test-support'; -import { module, skip, test } from 'qunit'; - -import { ensureTrailingSlash } from '@cardstack/runtime-common'; - -import ListingCreateCommand from '@cardstack/host/commands/listing-create'; -import ListingInstallCommand from '@cardstack/host/commands/listing-install'; -import ListingRemixCommand from '@cardstack/host/commands/listing-remix'; -import ListingUseCommand from '@cardstack/host/commands/listing-use'; - -import ENV from '@cardstack/host/config/environment'; - -import type { CardDef } from 'https://cardstack.com/base/card-api'; - -import { - setupLocalIndexing, - setupOnSave, - testRealmURL as mockCatalogURL, - setupAuthEndpoints, - setupUserSubscription, - setupAcceptanceTestRealm, - SYSTEM_CARD_FIXTURE_CONTENTS, - visitOperatorMode, - verifySubmode, - toggleFileTree, - openDir, - verifyFolderWithUUIDInFileTree, - verifyFileInFileTree, - verifyJSONWithUUIDInFolder, - setupRealmServerEndpoints, -} from '../helpers'; -import { setupMockMatrix } from '../helpers/mock-matrix'; -import { setupApplicationTest } from '../helpers/setup'; - -import type { CardListing } from '@cardstack/catalog/listing/listing'; - -const catalogRealmURL = ensureTrailingSlash(ENV.resolvedCatalogRealmURL); -const testDestinationRealmURL = `http://test-realm/test2/`; - -//listing -const authorListingId = `${mockCatalogURL}Listing/author`; -const personListingId = `${mockCatalogURL}Listing/person`; -const emptyListingId = `${mockCatalogURL}Listing/empty`; -const pirateSkillListingId = `${mockCatalogURL}SkillListing/pirate-skill`; -const incompleteSkillListingId = `${mockCatalogURL}Listing/incomplete-skill`; -const apiDocumentationStubListingId = `${mockCatalogURL}Listing/api-documentation-stub`; -const themeListingId = `${mockCatalogURL}ThemeListing/cardstack-theme`; -const blogPostListingId = `${mockCatalogURL}Listing/blog-post`; -//license -const mitLicenseId = `${mockCatalogURL}License/mit`; -//category -const writingCategoryId = `${mockCatalogURL}Category/writing`; -//publisher -const publisherId = `${mockCatalogURL}Publisher/boxel-publisher`; - -//skills -const pirateSkillId = `${mockCatalogURL}Skill/pirate-speak`; - -//tags -const calculatorTagId = `${mockCatalogURL}Tag/c1fe433a-b3df-41f4-bdcf-d98686ee42d7`; -const gameTagId = `${mockCatalogURL}Tag/51de249c-516a-4c4d-bd88-76e88274c483`; -const stubTagId = `${mockCatalogURL}Tag/stub`; - -//specs -const authorSpecId = `${mockCatalogURL}Spec/author`; -const unknownSpecId = `${mockCatalogURL}Spec/unknown-no-type`; - -//examples -const authorExampleId = `${mockCatalogURL}author/Author/example`; -const authorCompanyExampleId = `${mockCatalogURL}author/AuthorCompany/example`; - -const authorCardSource = ` - import { field, contains, linksTo, CardDef } from 'https://cardstack.com/base/card-api'; - import StringField from 'https://cardstack.com/base/string'; - - - export class AuthorCompany extends CardDef { - static displayName = 'AuthorCompany'; - @field name = contains(StringField); - @field address = contains(StringField); - @field city = contains(StringField); - @field state = contains(StringField); - @field zip = contains(StringField); - } - - export class Author extends CardDef { - static displayName = 'Author'; - @field firstName = contains(StringField); - @field lastName = contains(StringField); - @field cardTitle = contains(StringField, { - computeVia: function (this: Author) { - return [this.firstName, this.lastName].filter(Boolean).join(' '); - }, - }); - @field company = linksTo(AuthorCompany); - } -`; - -const blogPostCardSource = ` - import { field, contains, CardDef, linksTo } from 'https://cardstack.com/base/card-api'; - import StringField from 'https://cardstack.com/base/string'; - import { Author } from '../author/author'; - - export class BlogPost extends CardDef { - static displayName = 'BlogPost'; - @field cardTitle = contains(StringField); - @field content = contains(StringField); - @field author = linksTo(Author); - } -`; - -const contactLinkFieldSource = ` - import { field, contains, FieldDef } from 'https://cardstack.com/base/card-api'; - import StringField from 'https://cardstack.com/base/string'; - - export class ContactLink extends FieldDef { - static displayName = 'ContactLink'; - @field label = contains(StringField); - @field url = contains(StringField); - @field type = contains(StringField); - } -`; - -const appCardSource = ` - import { CardDef } from 'https://cardstack.com/base/card-api'; - - export class AppCard extends CardDef { - static displayName = 'App Card'; - static prefersWideFormat = true; - } -`; - -const blogAppCardSource = ` - import { field, contains, containsMany } from 'https://cardstack.com/base/card-api'; - import StringField from 'https://cardstack.com/base/string'; - import { AppCard } from '../app-card'; - import { BlogPost } from '../blog-post/blog-post'; - - export class BlogApp extends AppCard { - static displayName = 'Blog App'; - @field cardTitle = contains(StringField); - @field posts = containsMany(BlogPost); - } -`; - -const cardWithUnrecognisedImports = ` - import { field, CardDef, linksTo } from 'https://cardstack.com/base/card-api'; - // External import that should be ignored by sanitizeDeps - import { Chess as _ChessJS } from 'https://cdn.jsdelivr.net/npm/chess.js/+esm'; - import { Author } from './author/author'; - - export class UnrecognisedImports extends CardDef { - static displayName = 'Unrecognised Imports'; - @field author = linksTo(Author); - } -`; - -module('Acceptance | Catalog | catalog app tests', function (hooks) { - setupApplicationTest(hooks); - setupLocalIndexing(hooks); - setupOnSave(hooks); - - let mockMatrixUtils = setupMockMatrix(hooks, { - loggedInAs: '@testuser:localhost', - activeRealms: [mockCatalogURL, testDestinationRealmURL], - }); - - let { getRoomIds, createAndJoinRoom } = mockMatrixUtils; - - hooks.beforeEach(async function () { - createAndJoinRoom({ - sender: '@testuser:localhost', - name: 'room-test', - }); - setupUserSubscription(); - setupAuthEndpoints(); - // this setup test realm is pretending to be a mock catalog - await setupAcceptanceTestRealm({ - realmURL: mockCatalogURL, - mockMatrixUtils, - contents: { - ...SYSTEM_CARD_FIXTURE_CONTENTS, - 'author/author.gts': authorCardSource, - 'blog-post/blog-post.gts': blogPostCardSource, - 'fields/contact-link.gts': contactLinkFieldSource, - 'app-card.gts': appCardSource, - 'blog-app/blog-app.gts': blogAppCardSource, - 'card-with-unrecognised-imports.gts': cardWithUnrecognisedImports, - 'theme/theme-example.json': { - data: { - type: 'card', - attributes: { - cssVariables: - ':root { --background: #ffffff; } .dark { --background: #000000; }', - cssImports: [], - cardInfo: { - cardTitle: 'Sample Theme', - cardDescription: 'A sample theme for testing remix.', - cardThumbnailURL: null, - notes: null, - }, - }, - meta: { - adoptsFrom: { - module: 'https://cardstack.com/base/card-api', - name: 'Theme', - }, - }, - }, - }, - 'ThemeListing/cardstack-theme.json': { - data: { - meta: { - adoptsFrom: { - name: 'ThemeListing', - module: `${catalogRealmURL}catalog-app/listing/listing`, - }, - }, - type: 'card', - attributes: { - name: 'Cardstack Theme', - images: [], - summary: 'Cardstack base theme listing.', - }, - relationships: { - specs: { - links: { - self: null, - }, - }, - skills: { - links: { - self: null, - }, - }, - tags: { - links: { - self: null, - }, - }, - license: { - links: { - self: null, - }, - }, - publisher: { - links: { - self: null, - }, - }, - 'examples.0': { - links: { - self: '../theme/theme-example', - }, - }, - categories: { - links: { - self: null, - }, - }, - }, - }, - }, - 'author/Author/example.json': { - data: { - type: 'card', - attributes: { - firstName: 'Mike', - lastName: 'Dane', - summary: 'Author', - }, - relationships: { - company: { - links: { - self: authorCompanyExampleId, - }, - }, - }, - meta: { - adoptsFrom: { - module: `${mockCatalogURL}author/author`, - name: 'Author', - }, - }, - }, - }, - 'author/AuthorCompany/example.json': { - data: { - type: 'card', - attributes: { - name: 'Cardstack Labs', - address: '123 Main St', - city: 'Portland', - state: 'OR', - zip: '97205', - }, - meta: { - adoptsFrom: { - module: `${mockCatalogURL}author/author`, - name: 'AuthorCompany', - }, - }, - }, - }, - 'UnrecognisedImports/example.json': { - data: { - type: 'card', - attributes: {}, - meta: { - adoptsFrom: { - module: `${mockCatalogURL}card-with-unrecognised-imports`, - name: 'UnrecognisedImports', - }, - }, - }, - }, - 'blog-post/BlogPost/example.json': { - data: { - type: 'card', - attributes: { - cardTitle: 'Blog Post', - content: 'Blog Post Content', - }, - relationships: { - author: { - links: { - self: authorExampleId, - }, - }, - }, - meta: { - adoptsFrom: { - module: `${mockCatalogURL}blog-post/blog-post`, - name: 'BlogPost', - }, - }, - }, - }, - 'blog-app/BlogApp/example.json': { - data: { - type: 'card', - attributes: { - cardTitle: 'My Blog App', - }, - meta: { - adoptsFrom: { - module: `${mockCatalogURL}blog-app/blog-app`, - name: 'BlogApp', - }, - }, - }, - }, - 'Spec/author.json': { - data: { - type: 'card', - attributes: { - readMe: 'This is the author spec readme', - ref: { - name: 'Author', - module: `${mockCatalogURL}author/author`, - }, - }, - specType: 'card', - containedExamples: [], - cardTitle: 'Author', - cardDescription: 'Spec for Author card', - meta: { - adoptsFrom: { - module: 'https://cardstack.com/base/spec', - name: 'Spec', - }, - }, - }, - }, - 'Spec/contact-link.json': { - data: { - type: 'card', - attributes: { - ref: { - name: 'ContactLink', - module: `${mockCatalogURL}fields/contact-link`, - }, - }, - specType: 'field', - containedExamples: [], - cardTitle: 'ContactLink', - cardDescription: 'Spec for ContactLink field', - meta: { - adoptsFrom: { - module: 'https://cardstack.com/base/spec', - name: 'Spec', - }, - }, - }, - }, - 'Spec/unknown-no-type.json': { - data: { - type: 'card', - attributes: { - readMe: 'Spec without specType to trigger unknown grouping', - ref: { - name: 'UnknownNoType', - module: `${mockCatalogURL}unknown/unknown-no-type`, - }, - }, - // intentionally omitting specType so it falls into 'unknown' - containedExamples: [], - cardTitle: 'UnknownNoType', - cardDescription: 'Spec lacking specType', - meta: { - adoptsFrom: { - module: 'https://cardstack.com/base/spec', - name: 'Spec', - }, - }, - }, - }, - 'Listing/author.json': { - data: { - type: 'card', - attributes: { - name: 'Author', - cardTitle: 'Author', // hardcoding title otherwise test will be flaky when waiting for a computed - summary: 'A card for representing an author.', - }, - relationships: { - 'specs.0': { - links: { - self: authorSpecId, - }, - }, - 'examples.0': { - links: { - self: authorExampleId, - }, - }, - 'tags.0': { - links: { - self: calculatorTagId, - }, - }, - 'categories.0': { - links: { - self: writingCategoryId, - }, - }, - license: { - links: { - self: mitLicenseId, - }, - }, - publisher: { - links: { - self: publisherId, - }, - }, - }, - meta: { - adoptsFrom: { - module: `${catalogRealmURL}catalog-app/listing/listing`, - name: 'CardListing', - }, - }, - }, - }, - 'Listing/blog-post.json': { - data: { - type: 'card', - attributes: { - name: 'Blog Post', - cardTitle: 'Blog Post', - }, - relationships: { - 'examples.0': { - links: { - self: `${mockCatalogURL}blog-post/BlogPost/example`, - }, - }, - }, - meta: { - adoptsFrom: { - module: `${catalogRealmURL}catalog-app/listing/listing`, - name: 'CardListing', - }, - }, - }, - }, - 'Publisher/boxel-publisher.json': { - data: { - type: 'card', - attributes: { - name: 'Boxel Publishing', - }, - meta: { - adoptsFrom: { - module: `${catalogRealmURL}catalog-app/listing/publisher`, - name: 'Publisher', - }, - }, - }, - }, - 'License/mit.json': { - data: { - type: 'card', - attributes: { - name: 'MIT License', - content: 'MIT License', - }, - meta: { - adoptsFrom: { - module: `${catalogRealmURL}catalog-app/listing/license`, - name: 'License', - }, - }, - }, - }, - 'Listing/person.json': { - data: { - type: 'card', - attributes: { - name: 'Person', - cardTitle: 'Person', // hardcoding title otherwise test will be flaky when waiting for a computed - images: [ - 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400', - 'https://images.unsplash.com/photo-1494790108755-2616b332db29?w=400', - 'https://images.unsplash.com/photo-1552374196-c4e7ffc6e126?w=400', - ], - }, - relationships: { - 'tags.0': { - links: { - self: calculatorTagId, - }, - }, - }, - meta: { - adoptsFrom: { - module: `${catalogRealmURL}catalog-app/listing/listing`, - name: 'CardListing', - }, - }, - }, - }, - 'Listing/unknown-only.json': { - data: { - type: 'card', - attributes: {}, - relationships: { - 'specs.0': { - links: { - self: unknownSpecId, - }, - }, - }, - meta: { - adoptsFrom: { - module: `${catalogRealmURL}catalog-app/listing/listing`, - name: 'CardListing', - }, - }, - }, - }, - 'AppListing/blog-app.json': { - data: { - type: 'card', - attributes: { - name: 'Blog App', - cardTitle: 'Blog App', // hardcoding title otherwise test will be flaky when waiting for a computed - }, - meta: { - adoptsFrom: { - module: `${catalogRealmURL}catalog-app/listing/listing`, - name: 'AppListing', - }, - }, - }, - }, - 'Listing/empty.json': { - data: { - type: 'card', - attributes: { - name: 'Empty', - cardTitle: 'Empty', // hardcoding title otherwise test will be flaky when waiting for a computed - }, - meta: { - adoptsFrom: { - module: `${catalogRealmURL}catalog-app/listing/listing`, - name: 'CardListing', - }, - }, - }, - }, - 'SkillListing/pirate-skill.json': { - data: { - type: 'card', - attributes: { - name: 'Pirate Skill', - cardTitle: 'Pirate Skill', // hardcoding title otherwise test will be flaky when waiting for a computed - }, - relationships: { - 'skills.0': { - links: { - self: pirateSkillId, - }, - }, - }, - 'categories.0': { - links: { - self: writingCategoryId, - }, - }, - meta: { - adoptsFrom: { - module: `${catalogRealmURL}catalog-app/listing/listing`, - name: 'SkillListing', - }, - }, - }, - }, - 'Category/writing.json': { - data: { - type: 'card', - attributes: { - name: 'Writing', - }, - meta: { - adoptsFrom: { - module: `${catalogRealmURL}catalog-app/listing/category`, - name: 'Category', - }, - }, - }, - }, - 'Listing/incomplete-skill.json': { - data: { - type: 'card', - attributes: { - name: 'Incomplete Skill', - cardTitle: 'Incomplete Skill', // hardcoding title otherwise test will be flaky when waiting for a computed - }, - meta: { - adoptsFrom: { - module: `${catalogRealmURL}catalog-app/listing/listing`, - name: 'SkillListing', - }, - }, - }, - }, - 'Skill/pirate-speak.json': { - data: { - type: 'card', - attributes: { - cardTitle: 'Talk Like a Pirate', - name: 'Pirate Speak', - }, - meta: { - adoptsFrom: { - module: 'https://cardstack.com/base/skill', - name: 'Skill', - }, - }, - }, - }, - 'Tag/c1fe433a-b3df-41f4-bdcf-d98686ee42d7.json': { - data: { - type: 'card', - attributes: { - name: 'Calculator', - }, - meta: { - adoptsFrom: { - module: `${catalogRealmURL}catalog-app/listing/tag`, - name: 'Tag', - }, - }, - }, - }, - 'Tag/51de249c-516a-4c4d-bd88-76e88274c483.json': { - data: { - type: 'card', - attributes: { - name: 'Game', - }, - meta: { - adoptsFrom: { - module: `${catalogRealmURL}catalog-app/listing/tag`, - name: 'Tag', - }, - }, - }, - }, - 'Tag/stub.json': { - data: { - type: 'card', - attributes: { - name: 'Stub', - }, - meta: { - adoptsFrom: { - module: `${catalogRealmURL}catalog-app/listing/tag`, - name: 'Tag', - }, - }, - }, - }, - 'Listing/api-documentation-stub.json': { - data: { - type: 'card', - attributes: { - name: 'API Documentation', - cardTitle: 'API Documentation', // hardcoding title otherwise test will be flaky when waiting for a computed - }, - relationships: { - 'tags.0': { - links: { - self: stubTagId, - }, - }, - }, - meta: { - adoptsFrom: { - module: `${catalogRealmURL}catalog-app/listing/listing`, - name: 'Listing', - }, - }, - }, - }, - 'FieldListing/contact-link.json': { - data: { - type: 'card', - attributes: { - name: 'Contact Link', - cardTitle: 'Contact Link', // hardcoding title otherwise test will be flaky when waiting for a computed - summary: - 'A field for creating and managing contact links such as email, phone, or other web links.', - }, - relationships: { - 'specs.0': { - links: { - self: `${mockCatalogURL}Spec/contact-link`, - }, - }, - }, - meta: { - adoptsFrom: { - module: `${catalogRealmURL}catalog-app/listing/listing`, - name: 'FieldListing', - }, - }, - }, - }, - 'index.json': { - data: { - type: 'card', - attributes: {}, - relationships: { - 'startHere.0': { - links: { - self: authorListingId, - }, - }, - }, - meta: { - adoptsFrom: { - module: `${catalogRealmURL}catalog-app/catalog`, - name: 'Catalog', - }, - }, - }, - }, - '.realm.json': { - name: 'Cardstack Catalog', - backgroundURL: - 'https://i.postimg.cc/VNvHH93M/pawel-czerwinski-Ly-ZLa-A5jti-Y-unsplash.jpg', - iconURL: 'https://i.postimg.cc/L8yXRvws/icon.png', - }, - }, - }); - await setupAcceptanceTestRealm({ - mockMatrixUtils, - realmURL: testDestinationRealmURL, - contents: { - ...SYSTEM_CARD_FIXTURE_CONTENTS, - 'index.json': { - data: { - type: 'card', - meta: { - adoptsFrom: { - module: 'https://cardstack.com/base/cards-grid', - name: 'CardsGrid', - }, - }, - }, - }, - '.realm.json': { - name: 'Test Workspace B', - backgroundURL: - 'https://i.postimg.cc/VNvHH93M/pawel-czerwinski-Ly-ZLa-A5jti-Y-unsplash.jpg', - iconURL: 'https://i.postimg.cc/L8yXRvws/icon.png', - }, - }, - }); - }); - - /** - * Selects a tab by name within the catalog app - */ - async function selectTab(tabName: string) { - await waitFor(`[data-test-catalog-app] [data-test-tab-label="${tabName}"]`); - await click(`[data-test-catalog-app] [data-test-tab-label="${tabName}"]`); - } - - /** - * Waits for grid to load in the catalog app - */ - async function waitForGrid() { - await waitFor('[data-test-catalog-list-view]'); - await waitFor('[data-test-cards-grid-cards]'); - await settled(); - } - - /** - * Waits for showcase view to load - */ - async function waitForShowcase() { - await waitFor('[data-test-showcase-view]'); - await settled(); - } - - /** - * Waits for room operations to complete - */ - async function waitForRoom() { - await waitFor('[data-room-settled]'); - await settled(); - } - - /** - * Waits for a card to appear on the grid with optional title verification - */ - async function waitForCardOnGrid(cardId: string, title?: string) { - await waitFor(`[data-test-cards-grid-item="${cardId}"]`); - if (title) { - await waitFor( - `[data-test-cards-grid-item="${cardId}"] [data-test-card-title="${title}"]`, - //its problematic when we are waiting for computed title - //my recommendation for the purposes of test is to populate the card title in the realm - ); - } - } - - /** - * Waits for a card to appear on the stack with optional title verification - */ - async function waitForCardOnStack(cardId: string, expectedTitle?: string) { - await waitFor( - `[data-test-stack-card="${cardId}"] [data-test-boxel-card-header-title]`, - ); - if (expectedTitle) { - await waitFor( - `[data-test-stack-card="${cardId}"] [data-test-boxel-card-header-title]`, - ); - } - } - - async function clickDropdownItem(menuItemText: string) { - let selector = `[data-test-boxel-dropdown-content] [data-test-boxel-menu-item-text="${menuItemText}"]`; - await waitFor(selector); - await click(selector); - } - - async function hoverToHydrateCard(buttonSelector: string) { - await waitFor(buttonSelector); - await triggerEvent(buttonSelector, 'mouseenter'); - await waitFor('[data-test-hydrated-card]'); - } - - async function openMenu(buttonSelector: string, checkHydration = true) { - await waitFor(buttonSelector); - await triggerEvent(buttonSelector, 'mouseenter'); - if (checkHydration) { - await waitFor('[data-test-hydrated-card]'); - } - await click(buttonSelector); - } - - async function executeListingAction( - buttonSelector: string, - menuItemText: string, - checkHydration = true, - ) { - await openMenu(buttonSelector, checkHydration); - await clickDropdownItem(menuItemText); - } - - async function verifyListingAction( - assert: Assert, - buttonSelector: string, - expectedText: string, - expectedMessage: string, - menuItemName = 'Test Workspace B', - checkHydration = true, - ) { - await waitFor(buttonSelector); - assert.dom(buttonSelector).containsText(expectedText); - await executeListingAction(buttonSelector, menuItemName, checkHydration); - await waitUntil(() => getRoomIds().length > 0); - - const roomId = getRoomIds().pop()!; - await waitFor(`[data-test-room="${roomId}"][data-test-room-settled]`); - await waitFor( - `[data-test-room="${roomId}"] [data-test-ai-assistant-message]`, - ); - - await waitFor( - `[data-test-room="${roomId}"] [data-test-ai-message-content]`, - ); - await settled(); - - assert - .dom(`[data-test-room="${roomId}"] [data-test-ai-message-content]`) - .containsText(expectedMessage); - } - - async function assertDropdownItem( - assert: Assert, - menuItemText: string, - exists = true, - ) { - let selector = `[data-test-boxel-dropdown-content] [data-test-boxel-menu-item-text="${menuItemText}"]`; - if (exists) { - await waitFor(selector); - assert.dom(selector).exists(); - } else { - assert.dom(selector).doesNotExist(); - } - } - - async function executeCommand( - commandClass: - | typeof ListingUseCommand - | typeof ListingInstallCommand - | typeof ListingRemixCommand, - listingUrl: string, - realm: string, - ) { - const commandService = getService('command-service'); - const store = getService('store'); - - const command = new commandClass(commandService.commandContext); - const listing = (await store.get(listingUrl)) as CardDef; - - return command.execute({ - realm, - listing, - }); - } - - module('catalog index', function (hooks) { - hooks.beforeEach(async function () { - await visitOperatorMode({ - stacks: [ - [ - { - id: `${mockCatalogURL}index`, - format: 'isolated', - }, - ], - ], - }); - await waitForShowcase(); - }); - - module('listing fitted', function () { - test('after clicking "Remix" button, the ai room is initiated, and prompt is given correctly', async function (assert) { - await selectTab('Cards'); - await waitForGrid(); - await waitForCardOnGrid(authorListingId, 'Author'); - assert - .dom( - `[data-test-cards-grid-item="${authorListingId}"] [data-test-card-title="Author"]`, - ) - .containsText('Author', '"Author" exist in listing'); - await verifyListingAction( - assert, - `[data-test-cards-grid-item="${authorListingId}"] [data-test-catalog-listing-action="Remix"]`, - 'Remix', - 'Remix done! Please suggest two example prompts on how to edit this card.', - 'Test Workspace B', - ); - }); - - test('after clicking "Remix" button, current realm (particularly catalog realm) is never displayed in realm options', async function (assert) { - await selectTab('Cards'); - await waitForGrid(); - const listingId = mockCatalogURL + 'Listing/author'; - await waitFor(`[data-test-cards-grid-item="${listingId}"]`); - await waitFor( - `[data-test-cards-grid-item="${listingId}"] [data-test-card-title="Author"]`, - ); - await openMenu( - `[data-test-cards-grid-item="${listingId}"] [data-test-catalog-listing-action="Remix"]`, - ); - assert - .dom('[data-test-boxel-dropdown-content] [data-test-boxel-menu-item]') - .exists({ count: 1 }); - await assertDropdownItem(assert, 'Test Workspace B'); - await assertDropdownItem(assert, 'Test Workspace A', false); - }); - - test('after clicking "Preview" button, the first example card opens up onto the stack', async function (assert) { - await waitForCardOnGrid(authorListingId, 'Author'); - assert - .dom( - `[data-test-card="${authorListingId}"] [data-test-card-title="Author"]`, - ) - .containsText('Author', '"Author" button exist in listing'); - await click( - `[data-test-cards-grid-item="${authorListingId}"] [data-test-catalog-listing-fitted-preview-button]`, - ); - await waitForCardOnStack(`${mockCatalogURL}author/Author/example`); - assert - .dom( - `[data-test-stack-card="${mockCatalogURL}author/Author/example"] [data-test-boxel-card-header-title]`, - ) - .hasText('Author - Mike Dane'); - }); - - test('after clicking "Use Skills" button, the skills is attached to the skill menu', async function (assert) { - await selectTab('Skills'); - await waitForGrid(); - await waitFor(`[data-test-cards-grid-item="${pirateSkillListingId}"]`); - await openMenu( - `[data-test-cards-grid-item="${pirateSkillListingId}"] [data-test-catalog-listing-fitted-add-skills-to-room-button]`, - ); - await waitForRoom(); - await click('[data-test-skill-menu][data-test-pill-menu-button]'); - await waitFor('[data-test-skill-menu]'); - assert.dom('[data-test-skill-menu]').exists('Skill menu is visible'); - assert - .dom('[data-test-pill-menu-item]') - .containsText('Talk Like a Pirate') - .exists('Skill is attached to the skill menu'); - }); - - test('after clicking "carousel" area, the first example card opens up onto the stack', async function (assert) { - await waitForCardOnGrid(authorListingId, 'Author'); - assert - .dom( - `[data-test-card="${authorListingId}"] [data-test-card-title="Author"]`, - ) - .containsText('Author', '"Author" button exist in listing'); - await click( - `[data-test-cards-grid-item="${authorListingId}"] [data-test-catalog-listing-fitted-preview-button]`, - ); - await waitForCardOnStack(`${mockCatalogURL}author/Author/example`); - assert - .dom( - `[data-test-stack-card="${mockCatalogURL}author/Author/example"] [data-test-boxel-card-header-title]`, - ) - .hasText('Author - Mike Dane'); - }); - - test('after clicking "Details" button, the listing details card opens up onto the stack', async function (assert) { - await click( - `[data-test-cards-grid-item="${authorListingId}"] [data-test-catalog-listing-fitted-details-button]`, - ); - await waitForCardOnStack(authorListingId); - assert - .dom( - `[data-test-stack-card="${authorListingId}"] [data-test-boxel-card-header-title]`, - ) - .hasText('CardListing - Author'); - }); - - test('after clicking "info-section" area, the listing details card opens up onto the stack', async function (assert) { - await click( - `[data-test-card="${authorListingId}"] [data-test-catalog-listing-fitted-details]`, - ); - await waitForCardOnStack(authorListingId); - assert - .dom( - `[data-test-stack-card="${authorListingId}"] [data-test-boxel-card-header-title]`, - ) - .hasText('CardListing - Author'); - }); - - test('no arrows and dots appear when one or less image exist', async function (assert) { - await selectTab('Cards'); - await waitForGrid(); - await waitForCardOnGrid(emptyListingId); - await hoverToHydrateCard( - `[data-test-cards-grid-item="${emptyListingId}"]`, - ); - - const carouselNav = document.querySelector( - `[data-test-cards-grid-item="${emptyListingId}"] .carousel-nav`, - ); - const carouselDots = document.querySelector( - `[data-test-cards-grid-item="${emptyListingId}"] .carousel-dots`, - ); - - if (carouselNav && carouselDots) { - assert - .dom( - `[data-test-cards-grid-item="${emptyListingId}"] .carousel-arrow-prev`, - ) - .exists(); - assert - .dom( - `[data-test-cards-grid-item="${emptyListingId}"] .carousel-arrow-next`, - ) - .exists(); - assert - .dom( - `[data-test-cards-grid-item="${emptyListingId}"] .carousel-item-0.is-active`, - ) - .exists(); - } else { - assert - .dom( - `[data-test-cards-grid-item="${emptyListingId}"] .carousel-nav`, - ) - .doesNotExist(); - assert - .dom( - `[data-test-cards-grid-item="${emptyListingId}"] .carousel-dots`, - ) - .doesNotExist(); - assert - .dom( - `[data-test-cards-grid-item="${emptyListingId}"] .carousel-arrow-prev`, - ) - .doesNotExist(); - assert - .dom( - `[data-test-cards-grid-item="${emptyListingId}"] .carousel-arrow-next`, - ) - .doesNotExist(); - } - }); - - test('carousel arrows only when multiple images exist and works when triggered', async function (assert) { - await selectTab('Cards'); - await waitForGrid(); - await waitForCardOnGrid(personListingId); - await hoverToHydrateCard( - `[data-test-cards-grid-item="${personListingId}"]`, - ); - - await click( - `[data-test-cards-grid-item="${personListingId}"] .carousel-arrow-prev`, - ); - assert - .dom( - `[data-test-cards-grid-item="${personListingId}"] .carousel-item-2.is-active`, - ) - .exists('After clicking prev, last slide (index 2) is active'); - - await click( - `[data-test-cards-grid-item="${personListingId}"] .carousel-arrow-next`, - ); - assert - .dom( - `[data-test-cards-grid-item="${personListingId}"] .carousel-item-0.is-active`, - ) - .exists('After clicking next, first slide (index 0) is active'); - }); - - test('carousel dots appear only when multiple images exist and works when triggered', async function (assert) { - await selectTab('Cards'); - await waitForGrid(); - await waitForCardOnGrid(personListingId); - - // Hover over the carousel to make controls visible - await hoverToHydrateCard( - `[data-test-cards-grid-item="${personListingId}"]`, - ); - - const dots = document.querySelectorAll( - `[data-test-cards-grid-item="${personListingId}"] .carousel-dot`, - ); - - if (dots.length > 1) { - await click(dots[1]); - assert - .dom( - `[data-test-cards-grid-item="${personListingId}"] .carousel-item-1.is-active`, - ) - .exists('After clicking dot 1, slide 1 is active'); - } - }); - - test('preview button appears only when examples exist', async function (assert) { - await selectTab('Cards'); - await waitForGrid(); - await waitForCardOnGrid(authorListingId); - await hoverToHydrateCard( - `[data-test-cards-grid-item="${authorListingId}"]`, - ); - const previewButton = document.querySelector( - `[data-test-cards-grid-item="${authorListingId}"] [data-test-catalog-listing-fitted-preview-button]`, - ); - - if (previewButton) { - assert - .dom( - `[data-test-cards-grid-item="${authorListingId}"] [data-test-catalog-listing-fitted-preview-button]`, - ) - .exists(); - } else { - assert - .dom( - `[data-test-cards-grid-item="${authorListingId}"] [data-test-catalog-listing-fitted-preview-button]`, - ) - .doesNotExist(); - } - }); - }); - - module('navigation', function () { - // showcase tab has different behavior compared to other tabs (apps, cards, fields, skills) - module('show results as per catalog tab selected', function () { - test('switch to showcase tab', async function (assert) { - await selectTab('Showcase'); - await waitForShowcase(); - assert - .dom('[data-test-navigation-reset-button="showcase"]') - .exists(`"Catalog Home" button should exist`) - .hasClass('is-selected'); - assert.dom('[data-test-boxel-radio-option-id="grid"]').doesNotExist(); - }); - - test('switch to apps tab', async function (assert) { - await selectTab('Apps'); - await waitForGrid(); - assert - .dom('[data-test-navigation-reset-button="app"]') - .exists(`"All Apps" button should exist`) - .hasClass('is-selected'); - assert.dom('[data-test-boxel-radio-option-id="grid"]').exists(); - }); - }); - - skip('filters', async function () { - test('list view is shown if filters are applied', async function (assert) { - await waitFor('[data-test-filter-search-input]'); - await click('[data-test-filter-search-input]'); - await fillIn('[data-test-filter-search-input]', 'Mortgage'); - // filter by category - await click('[data-test-filter-list-item="All"]'); - // filter by tag - let tagPill = document.querySelector('[data-test-tag-list-pill]'); - if (tagPill) { - await click(tagPill); - } - - await waitUntil(() => { - const cards = document.querySelectorAll( - '[data-test-catalog-list-view]', - ); - return cards.length === 1; - }); - - assert - .dom('[data-test-catalog-list-view]') - .exists( - 'Catalog list view should be visible when filters are applied', - ); - }); - - // TOOD: restore in CS-9083 - skip('should be reset when clicking "Catalog Home" button', async function (assert) { - await waitFor('[data-test-filter-search-input]'); - await click('[data-test-filter-search-input]'); - await fillIn('[data-test-filter-search-input]', 'Mortgage'); - // filter by category - await click('[data-test-filter-list-item="All"]'); - // filter by tag - let tagPill = document.querySelector('[data-test-tag-list-pill]'); - if (tagPill) { - await click(tagPill); - } - - assert - .dom('[data-test-showcase-view]') - .doesNotExist('Should be in list view after applying filter'); - - await click('[data-test-navigation-reset-button="showcase"]'); - - assert - .dom('[data-test-showcase-view]') - .exists( - 'Should return to showcase view after clicking Catalog Home', - ); - - assert - .dom('[data-test-filter-search-input]') - .hasValue('', 'Search input should be cleared'); - assert - .dom('[data-test-filter-list-item].is-selected') - .doesNotExist('No category should be selected after reset'); - assert - .dom('[data-test-tag-list-pill].selected') - .doesNotExist('No tag should be selected after reset'); - }); - - // TODO: restore in CS-9131 - skip('should be reset when clicking "All Apps" button', async function (assert) { - await selectTab('Apps'); - await waitForGrid(); - - await waitFor('[data-test-filter-search-input]'); - await click('[data-test-filter-search-input]'); - await fillIn('[data-test-filter-search-input]', 'Mortgage'); - // filter by category - await click('[data-test-filter-list-item="All"]'); - // filter by tag - let tagPill = document.querySelector('[data-test-tag-list-pill]'); - if (tagPill) { - await click(tagPill); - } - - await click('[data-test-navigation-reset-button="app"]'); - assert - .dom('[data-test-showcase-view]') - .doesNotExist('Should remain in list view, not return to showcase'); - await waitUntil(() => { - const cards = document.querySelectorAll( - '[data-test-catalog-list-view]', - ); - return cards.length === 1; - }); - assert - .dom('[data-test-catalog-list-view]') - .exists('Catalog list view should still be visible'); - - assert - .dom('[data-test-filter-search-input]') - .hasValue('', 'Search input should be cleared'); - assert - .dom('[data-test-filter-list-item].is-selected') - .doesNotExist('No category should be selected after reset'); - assert - .dom('[data-test-tag-list-pill].selected') - .doesNotExist('No tag should be selected after reset'); - }); - - skip('updates the card count correctly when filtering by a sphere group', async function (assert) { - await click('[data-test-boxel-filter-list-button="LIFE"]'); - assert - .dom('[data-test-cards-grid-cards] [data-test-cards-grid-item]') - .exists({ count: 2 }); - }); - - skip('updates the card count correctly when filtering by a category', async function (assert) { - await click('[data-test-filter-list-item="LIFE"] .dropdown-toggle'); - await click( - '[data-test-boxel-filter-list-button="Health & Wellness"]', - ); - assert - .dom('[data-test-cards-grid-cards] [data-test-cards-grid-item]') - .exists({ count: 1 }); - }); - - skip('updates the card count correctly when filtering by a search input', async function (assert) { - await click('[data-test-filter-search-input]'); - await fillIn('[data-test-filter-search-input]', 'Mortgage'); - await waitUntil(() => { - const cards = document.querySelectorAll( - '[data-test-cards-grid-cards] [data-test-cards-grid-item]', - ); - return cards.length === 1; - }); - assert - .dom('[data-test-cards-grid-cards] [data-test-cards-grid-item]') - .exists({ count: 1 }); - }); - - test('updates the card count correctly when filtering by a single tag', async function (assert) { - await click(`[data-test-tag-list-pill="${gameTagId}"]`); - assert - .dom(`[data-test-tag-list-pill="${gameTagId}"]`) - .hasClass('selected'); - assert - .dom('[data-test-cards-grid-cards] [data-test-cards-grid-item]') - .exists({ count: 1 }); - }); - - test('updates the card count correctly when filtering by multiple tags', async function (assert) { - await click(`[data-test-tag-list-pill="${calculatorTagId}"]`); - await click(`[data-test-tag-list-pill="${gameTagId}"]`); - assert - .dom('[data-test-cards-grid-cards] [data-test-cards-grid-item]') - .exists({ count: 2 }); - }); - - test('updates the card count correctly when multiple filters are applied together', async function (assert) { - await click('[data-test-boxel-filter-list-button="All"]'); - await click(`[data-test-tag-list-pill="${gameTagId}"]`); - await click('[data-test-filter-search-input]'); - await fillIn('[data-test-filter-search-input]', 'Blackjack'); - - await waitUntil(() => { - const cards = document.querySelectorAll( - '[data-test-cards-grid-cards] [data-test-cards-grid-item]', - ); - return cards.length === 1; - }); - - assert - .dom('[data-test-cards-grid-cards] [data-test-cards-grid-item]') - .exists({ count: 1 }); - }); - - test('shows zero results when filtering with a non-matching or invalid search input', async function (assert) { - await click('[data-test-filter-search-input]'); - await fillIn('[data-test-filter-search-input]', 'asdfasdf'); - await waitUntil(() => { - const cards = document.querySelectorAll('[data-test-no-results]'); - return cards.length === 1; - }); - - assert.dom('[data-test-no-results]').exists(); - }); - - test('categories with null sphere fields are excluded from filter list', async function (assert) { - // Setup: Create a category with null sphere field - await setupAcceptanceTestRealm({ - realmURL: mockCatalogURL, - mockMatrixUtils, - contents: { - ...SYSTEM_CARD_FIXTURE_CONTENTS, - 'Category/category-with-null-sphere.json': { - data: { - type: 'card', - attributes: { - name: 'CategoryWithNullSphere', - }, - relationships: { - sphere: { - links: { - self: null, - }, - }, - }, - meta: { - adoptsFrom: { - module: `${mockCatalogURL}catalog-app/listing/category`, - name: 'Category', - }, - }, - }, - }, - }, - }); - - await visitOperatorMode({ - stacks: [ - [ - { - id: `${mockCatalogURL}`, - format: 'isolated', - }, - ], - ], - }); - - assert - .dom( - '[data-test-boxel-filter-list-button="CategoryWithNullSphere"]', - ) - .doesNotExist( - 'Category with null sphere should not appear in filter list', - ); - }); - }); - }); - }); - - module('listing isolated', function (hooks) { - hooks.beforeEach(async function () { - await visitOperatorMode({ - stacks: [ - [ - { - id: authorListingId, - format: 'isolated', - }, - ], - ], - }); - }); - - test('listing card shows more options dropdown in stack item', async function (assert) { - let triggerSelector = `[data-test-stack-card="${authorListingId}"] [data-test-more-options-button]`; - await waitFor(triggerSelector); - await click(triggerSelector); - await waitFor('[data-test-boxel-dropdown-content]'); - assert - .dom('[data-test-boxel-dropdown-content] [data-test-boxel-menu-item]') - .exists('Listing card dropdown renders menu items'); - assert - .dom( - `[data-test-boxel-dropdown-content] [data-test-boxel-menu-item-text="Generate Example with AI"]`, - ) - .exists('Generate Example with AI action is present'); - }); - - test('after clicking "Remix" button, current realm (particularly catalog realm) is never displayed in realm options', async function (assert) { - let selector = `[data-test-card="${authorListingId}"] [data-test-catalog-listing-action="Remix"]`; - await openMenu(selector, false); - assert - .dom('[data-test-boxel-dropdown-content] [data-test-boxel-menu-item]') - .exists({ count: 1 }); - await assertDropdownItem(assert, 'Test Workspace B'); - await assertDropdownItem(assert, 'Test Workspace A', false); - }); - - test('after clicking "Use Skills" button, the skills is attached to the skill menu', async function (assert) { - await visitOperatorMode({ - stacks: [ - [ - { - id: pirateSkillListingId, - format: 'isolated', - }, - ], - ], - }); - await click( - '[data-test-catalog-listing-embedded-add-skills-to-room-button]', - ); - - await waitFor('[data-room-settled]'); - await click('[data-test-skill-menu][data-test-pill-menu-button]'); - await waitFor('[data-test-skill-menu]'); - assert.dom('[data-test-skill-menu]').exists('Skill menu is visible'); - assert - .dom('[data-test-pill-menu-item]') - .containsText('Talk Like a Pirate') - .exists('Skill is attached to the skill menu'); - }); - - test('after clicking "Remix" button, the ai room is initiated, and prompt is given correctly', async function (assert) { - await verifyListingAction( - assert, - `[data-test-card="${authorListingId}"] [data-test-catalog-listing-action="Remix"]`, - 'Remix', - 'Remix done! Please suggest two example prompts on how to edit this card.', - 'Test Workspace B', - false, - ); - }); - - test('after clicking "Preview" button, the first example card opens up onto the stack', async function (assert) { - await click( - `[data-test-card="${authorListingId}"] [data-test-catalog-listing-embedded-preview-button]`, - ); - await waitForCardOnStack(`${mockCatalogURL}author/Author/example`); - assert - .dom( - `[data-test-stack-card="${mockCatalogURL}author/Author/example"] [data-test-boxel-card-header-title]`, - ) - .hasText('Author - Mike Dane'); - }); - - test('display of sections when viewing listing details', async function (assert) { - await visitOperatorMode({ - stacks: [ - [ - { - id: authorListingId, - format: 'isolated', - }, - ], - ], - }); - - assert - .dom('[data-test-catalog-listing-embedded-summary-section]') - .containsText('A card for representing an author'); - - // Publisher (rendered in header) - assert - .dom('[data-test-app-listing-header-publisher]') - .containsText('By Boxel Publishing'); - - assert - .dom('[data-test-catalog-listing-embedded-license-section]') - .containsText('MIT License'); - - assert - .dom('[data-test-catalog-listing-embedded-images-section]') - .exists({ count: 1 }); - assert - .dom('[data-test-catalog-listing-embedded-examples-section]') - .exists(); - - assert - .dom('[data-test-catalog-listing-embedded-examples] li') - .exists({ count: 1 }); - assert.dom('[data-test-catalog-listing-embedded-tags-section]').exists(); - - //TODO: this assertion is wrong, there is some issue with rendering of specType - // also the format of the isolated has some weird css behaviour like Examples title running out of position - // assert - // .dom('[data-test-catalog-listing-embedded-specs-section]') - // .containsText('Unknown'); - - assert.dom('[data-test-catalog-listing-embedded-specs-section]').exists(); - - assert - .dom('[data-test-catalog-listing-embedded-tags-section]') - .containsText('Calculator'); - assert - .dom('[data-test-catalog-listing-embedded-categories-section]') - .containsText('Writing'); - }); - - test('listing with spec that has a missing specType groups it under unknown (accordion assertion)', async function (assert) { - const unknownListingId = `${mockCatalogURL}Listing/unknown-only`; - await visitOperatorMode({ - stacks: [ - [ - { - id: unknownListingId, - format: 'isolated', - }, - ], - ], - }); - - assert - .dom( - '[data-test-catalog-listing-embedded-specs-section] [data-test-accordion-item]', - ) - .exists({ count: 1 }); - assert - .dom( - '[data-test-catalog-listing-embedded-specs-section] [data-test-accordion-item="unknown"]', - ) - .exists('Unknown group item exists'); - - assert - .dom( - '[data-test-catalog-listing-embedded-specs-section] [data-test-accordion-item="unknown"]', - ) - .containsText('unknown (1)'); - }); - - test('unknown-only listing shows all default fallback texts', async function (assert) { - const unknownListingId = `${mockCatalogURL}Listing/unknown-only`; - await visitOperatorMode({ - stacks: [ - [ - { - id: unknownListingId, - format: 'isolated', - }, - ], - ], - }); - - assert - .dom('[data-test-catalog-listing-embedded-summary-section]') - .containsText('No Summary Provided'); - assert - .dom('[data-test-catalog-listing-embedded-license-section]') - .containsText('No License Provided'); - assert - .dom('[data-test-catalog-listing-embedded-images-section]') - .containsText('No Images Provided'); - assert - .dom('[data-test-catalog-listing-embedded-examples-section]') - .containsText('No Examples Provided'); - assert - .dom('[data-test-catalog-listing-embedded-categories-section]') - .containsText('No Categories Provided'); - assert - .dom('[data-test-catalog-listing-embedded-tags-section]') - .containsText('No Tags Provided'); - assert - .dom('[data-test-catalog-listing-embedded-skills-section]') - .containsText('No Skills Provided'); - }); - - test('remix button does not exist when a listing has no specs', async function (assert) { - await visitOperatorMode({ - stacks: [ - [ - { - id: emptyListingId, - format: 'isolated', - }, - ], - ], - }); - assert - .dom('[data-test-catalog-listing-embedded-specs-section]') - .containsText('No Specs Provided'); - assert.dom('[data-test-catalog-listing-action="Remix"]').doesNotExist(); - }); - - test('remix button does not exist when a skill listing has no skills', async function (assert) { - const emptySkillListingId = incompleteSkillListingId; - await visitOperatorMode({ - stacks: [ - [ - { - id: emptySkillListingId, - format: 'isolated', - }, - ], - ], - }); - assert - .dom('[data-test-catalog-listing-embedded-skills-section]') - .containsText('No Skills Provided'); - assert.dom('[data-test-catalog-listing-action="Remix"]').doesNotExist(); - }); - - test('after clicking "Build" button, the ai room is initiated, and prompt is given correctly', async function (assert) { - await visitOperatorMode({ - stacks: [ - [ - { - id: apiDocumentationStubListingId, - format: 'isolated', - }, - ], - ], - }); - await verifyListingAction( - assert, - `[data-test-card="${apiDocumentationStubListingId}"] [data-test-catalog-listing-action="Build"]`, - 'Build', - 'Generate .gts card definition for "API Documentation" implementing all requirements from the attached listing specification. Then preview the final code in playground panel.', - 'Test Workspace B', - false, - ); - }); - }); - - module('listing commands', function (hooks) { - hooks.beforeEach(async function () { - // we always run a command inside interact mode - await visitOperatorMode({ - stacks: [[]], - }); - }); - module('"build"', function () { - test('card listing', async function (assert) { - await visitOperatorMode({ - stacks: [ - [ - { - id: apiDocumentationStubListingId, - format: 'isolated', - }, - ], - ], - }); - await waitFor(`[data-test-card="${apiDocumentationStubListingId}"]`); - assert - .dom( - `[data-test-card="${apiDocumentationStubListingId}"] [data-test-catalog-listing-action="Build"]`, - ) - .containsText('Build', 'Build button exist in listing'); - }); - }); - module('"create"', function (hooks) { - // Mock proxy LLM endpoint only for create-related tests - setupRealmServerEndpoints(hooks, [ - { - route: '_request-forward', - getResponse: async (req: Request) => { - try { - const body = await req.json(); - if ( - body.url === 'https://openrouter.ai/api/v1/chat/completions' - ) { - let requestBody: any = {}; - try { - requestBody = body.requestBody - ? JSON.parse(body.requestBody) - : {}; - } catch { - // ignore parse failure - } - const messages = requestBody.messages || []; - const system: string = - messages.find((m: any) => m.role === 'system')?.content || ''; - const user: string = - messages.find((m: any) => m.role === 'user')?.content || ''; - const systemLower = system.toLowerCase(); - let content: string | undefined; - if ( - systemLower.includes( - 'respond only with one token: card, app, skill, or theme', - ) - ) { - // Heuristic moved from production code into test mock: - // If the serialized example or prompts reference an App construct - // (e.g. AppCard base class, module paths with /App/, or a name ending with App) - // then classify as 'app'. If it references Skill, classify as 'skill'. - const userLower = user.toLowerCase(); - if ( - /(appcard|blogapp|"appcard"|\.appcard|name: 'appcard')/.test( - userLower, - ) - ) { - content = 'app'; - } else if ( - /(cssvariables|css imports|theme card|themecreator|theme listing)/.test( - userLower, - ) - ) { - content = 'theme'; - } else if (/skill/.test(userLower)) { - content = 'skill'; - } else { - content = 'card'; - } - } else if (systemLower.includes('catalog listing title')) { - content = 'Mock Listing Title'; - } else if (systemLower.includes('spec-style summary')) { - content = 'Mock listing summary sentence.'; - } else if ( - systemLower.includes("boxel's sample data assistant") - ) { - content = JSON.stringify({ - examples: [ - { - label: 'Generated field value', - url: 'https://example.com/contact', - }, - ], - }); - } else if (systemLower.includes('representing tag')) { - // Deterministic tag selection - content = JSON.stringify([calculatorTagId]); - } else if (systemLower.includes('representing category')) { - // Deterministic category selection - content = JSON.stringify([writingCategoryId]); - } else if (systemLower.includes('representing license')) { - // Deterministic license selection - content = JSON.stringify([mitLicenseId]); - } - - return new Response( - JSON.stringify({ - choices: [ - { - message: { - content, - }, - }, - ], - }), - { - status: 200, - headers: { 'Content-Type': 'application/json' }, - }, - ); - } - } catch (e) { - return new Response( - JSON.stringify({ - error: 'mock forward error', - details: (e as Error).message, - }), - { - status: 500, - headers: { 'Content-Type': 'application/json' }, - }, - ); - } - return new Response( - JSON.stringify({ error: 'Unknown proxy path' }), - { - status: 404, - headers: { 'Content-Type': 'application/json' }, - }, - ); - }, - }, - ]); - test('card listing with single dependency module', async function (assert) { - const cardId = mockCatalogURL + 'author/Author/example'; - const commandService = getService('command-service'); - const command = new ListingCreateCommand(commandService.commandContext); - const result = await command.execute({ - openCardId: cardId, - codeRef: { - module: `${mockCatalogURL}author/author.gts`, - name: 'Author', - }, - targetRealm: mockCatalogURL, - }); - const interim = result?.listing as any; - assert.ok(interim, 'Interim listing exists'); - assert.strictEqual((interim as any).name, 'Mock Listing Title'); - assert.strictEqual( - (interim as any).summary, - 'Mock listing summary sentence.', - ); - await visitOperatorMode({ - submode: 'code', - fileView: 'browser', - codePath: `${mockCatalogURL}index`, - }); - await verifySubmode(assert, 'code'); - const instanceFolder = 'CardListing/'; - await openDir(assert, instanceFolder); - const listingId = await verifyJSONWithUUIDInFolder( - assert, - instanceFolder, - ); - if (listingId) { - const listing = (await getService('store').get( - listingId, - )) as CardListing; - assert.ok(listing, 'Listing should be created'); - // Assertions for AI generated fields coming from proxy mock - assert.strictEqual( - (listing as any).name, - 'Mock Listing Title', - 'Listing name populated from autoPatchName mock response', - ); - assert.strictEqual( - (listing as any).summary, - 'Mock listing summary sentence.', - 'Listing summary populated from autoPatchSummary mock response', - ); - assert.strictEqual( - listing.specs.length, - 2, - 'Listing should have two specs', - ); - assert.true( - listing.specs.some((spec) => spec.ref.name === 'Author'), - 'Listing should have an Author spec', - ); - assert.true( - listing.specs.some((spec) => spec.ref.name === 'AuthorCompany'), - 'Listing should have an AuthorCompany spec', - ); - // Deterministic autoLink assertions from proxy mock - assert.ok((listing as any).license, 'License linked'); - assert.strictEqual( - (listing as any).license.id, - mitLicenseId, - 'License id matches mitLicenseId', - ); - assert.ok(Array.isArray((listing as any).tags), 'Tags array exists'); - assert.true( - (listing as any).tags.some((t: any) => t.id === calculatorTagId), - 'Contains calculator tag id', - ); - assert.ok( - Array.isArray((listing as any).categories), - 'Categories array exists', - ); - assert.true( - (listing as any).categories.some( - (c: any) => c.id === writingCategoryId, - ), - 'Contains writing category id', - ); - } - }); - - test('listing will only create specs with recognised imports from realms it can read from', async function (assert) { - const cardId = mockCatalogURL + 'UnrecognisedImports/example'; - const commandService = getService('command-service'); - const command = new ListingCreateCommand(commandService.commandContext); - await command.execute({ - openCardId: cardId, - codeRef: { - module: `${mockCatalogURL}card-with-unrecognised-imports.gts`, - name: 'UnrecognisedImports', - }, - targetRealm: mockCatalogURL, - }); - await visitOperatorMode({ - submode: 'code', - fileView: 'browser', - codePath: `${mockCatalogURL}index`, - }); - await verifySubmode(assert, 'code'); - const instanceFolder = 'CardListing/'; - await openDir(assert, instanceFolder); - const listingId = await verifyJSONWithUUIDInFolder( - assert, - instanceFolder, - ); - if (listingId) { - const listing = (await getService('store').get( - listingId, - )) as CardListing; - assert.ok(listing, 'Listing should be created'); - assert.true( - listing.specs.every( - (spec) => - spec.ref.module != 'https://cdn.jsdelivr.net/npm/chess.js/+esm', - ), - 'Listing should does not have unrecognised import', - ); - } - }); - - test('app listing', async function (assert) { - const cardId = mockCatalogURL + 'blog-app/BlogApp/example'; - const commandService = getService('command-service'); - const command = new ListingCreateCommand(commandService.commandContext); - const createResult = await command.execute({ - openCardId: cardId, - codeRef: { - module: `${mockCatalogURL}blog-app/blog-app.gts`, - name: 'BlogApp', - }, - targetRealm: testDestinationRealmURL, - }); - // Assert store-level (in-memory) results BEFORE navigating to code mode - let immediateListing = createResult?.listing as any; - assert.ok(immediateListing, 'Listing object returned from command'); - assert.strictEqual( - immediateListing.name, - 'Mock Listing Title', - 'Immediate listing has patched name before persistence', - ); - assert.strictEqual( - immediateListing.summary, - 'Mock listing summary sentence.', - 'Immediate listing has patched summary before persistence', - ); - assert.ok( - immediateListing.license, - 'Immediate listing has linked license before persistence', - ); - assert.strictEqual( - immediateListing.license?.id, - mitLicenseId, - 'Immediate listing license id matches mitLicenseId', - ); - // Lint: avoid logical expression inside assertion - assert.ok( - Array.isArray(immediateListing.tags), - 'Immediate listing tags is an array before persistence', - ); - if (Array.isArray(immediateListing.tags)) { - assert.ok( - immediateListing.tags.length > 0, - 'Immediate listing has linked tag(s) before persistence', - ); - } - assert.true( - immediateListing.tags.some((t: any) => t.id === calculatorTagId), - 'Immediate listing includes calculator tag id', - ); - assert.ok( - Array.isArray(immediateListing.categories), - 'Immediate listing categories is an array before persistence', - ); - if (Array.isArray(immediateListing.categories)) { - assert.ok( - immediateListing.categories.length > 0, - 'Immediate listing has linked category(ies) before persistence', - ); - } - assert.true( - immediateListing.categories.some( - (c: any) => c.id === writingCategoryId, - ), - 'Immediate listing includes writing category id', - ); - assert.ok( - Array.isArray(immediateListing.specs), - 'Immediate listing specs is an array before persistence', - ); - if (Array.isArray(immediateListing.specs)) { - assert.strictEqual( - immediateListing.specs.length, - 5, - 'Immediate listing has expected number of specs before persistence', - ); - } - assert.ok( - Array.isArray(immediateListing.examples), - 'Immediate listing examples is an array before persistence', - ); - if (Array.isArray(immediateListing.examples)) { - assert.strictEqual( - immediateListing.examples.length, - 1, - 'Immediate listing has expected examples before persistence', - ); - } - // Header/title: wait for persisted id (listing.id) then assert via stack card selector - const persistedId = immediateListing.id; - assert.ok(persistedId, 'Immediate listing has a persisted id'); - await waitForCardOnStack(persistedId); - assert - .dom( - `[data-test-stack-card="${persistedId}"] [data-test-boxel-card-header-title]`, - ) - .containsText( - 'Mock Listing Title', - 'Isolated view shows patched name (persisted id)', - ); - // Summary section - assert - .dom('[data-test-catalog-listing-embedded-summary-section]') - .containsText( - 'Mock listing summary sentence.', - 'Isolated view shows patched summary', - ); - - // License section should not show fallback text - assert - .dom('[data-test-catalog-listing-embedded-license-section]') - .doesNotContainText( - 'No License Provided', - 'License section populated (autoLinkLicense)', - ); - - // Tags section - assert - .dom('[data-test-catalog-listing-embedded-tags-section]') - .doesNotContainText( - 'No Tags Provided', - 'Tags section populated (autoLinkTag)', - ); - - // Categories section - assert - .dom('[data-test-catalog-listing-embedded-categories-section]') - .doesNotContainText( - 'No Categories Provided', - 'Categories section populated (autoLinkCategory)', - ); - await visitOperatorMode({ - submode: 'code', - fileView: 'browser', - codePath: `${testDestinationRealmURL}index`, - }); - await verifySubmode(assert, 'code'); - const instanceFolder = 'AppListing/'; - await openDir(assert, instanceFolder); - const persistedListingId = await verifyJSONWithUUIDInFolder( - assert, - instanceFolder, - ); - if (persistedListingId) { - const listing = (await getService('store').get( - persistedListingId, - )) as CardListing; - assert.ok(listing, 'Listing should be created'); - assert.strictEqual( - listing.specs.length, - 5, - 'Listing should have five specs', - ); - ['Author', 'AuthorCompany', 'BlogPost', 'BlogApp', 'AppCard'].forEach( - (specName) => { - assert.true( - listing.specs.some((spec) => spec.ref.name === specName), - `Listing should have a ${specName} spec`, - ); - }, - ); - assert.strictEqual( - listing.examples.length, - 1, - 'Listing should have one example', - ); - - // Assert autoPatch fields populated (from proxy mock responses) - assert.strictEqual( - (listing as any).name, - 'Mock Listing Title', - 'autoPatchName populated listing.name', - ); - assert.strictEqual( - (listing as any).summary, - 'Mock listing summary sentence.', - 'autoPatchSummary populated listing.summary', - ); - - // Basic object-level sanity for autoLink fields (they should exist, may be arrays) - assert.ok( - (listing as any).license, - 'autoLinkLicense populated listing.license', - ); - assert.strictEqual( - (listing as any).license?.id, - mitLicenseId, - 'Persisted listing license id matches mitLicenseId', - ); - assert.ok( - Array.isArray((listing as any).tags), - 'autoLinkTag populated listing.tags array', - ); - if (Array.isArray((listing as any).tags)) { - assert.ok( - (listing as any).tags.length > 0, - 'autoLinkTag populated listing.tags with at least one tag', - ); - } - assert.true( - (listing as any).tags.some((t: any) => t.id === calculatorTagId), - 'Persisted listing includes calculator tag id', - ); - assert.ok( - Array.isArray((listing as any).categories), - 'autoLinkCategory populated listing.categories array', - ); - if (Array.isArray((listing as any).categories)) { - assert.ok( - (listing as any).categories.length > 0, - 'autoLinkCategory populated listing.categories with at least one category', - ); - } - assert.true( - (listing as any).categories.some( - (c: any) => c.id === writingCategoryId, - ), - 'Persisted listing includes writing category id', - ); - } - }); - - test('after create command, listing card opens on stack in interact mode', async function (assert) { - const cardId = mockCatalogURL + 'author/Author/example'; - const commandService = getService('command-service'); - const command = new ListingCreateCommand(commandService.commandContext); - - let r = await command.execute({ - openCardId: cardId, - codeRef: { - module: `${mockCatalogURL}author/author.gts`, - name: 'Author', - }, - targetRealm: mockCatalogURL, - }); - - await verifySubmode(assert, 'interact'); - const listing = r?.listing as any; - const createdId = listing.id; - assert.ok(createdId, 'Listing id should be present'); - await waitForCardOnStack(createdId); - assert - .dom(`[data-test-stack-card="${createdId}"]`) - .exists( - 'Created listing card (by persisted id) is displayed on stack after command execution', - ); - }); - }); - skip('"use"', async function () { - skip('card listing', async function (assert) { - const listingName = 'author'; - const listingId = mockCatalogURL + 'Listing/author.json'; - await executeCommand( - ListingUseCommand, - listingId, - testDestinationRealmURL, - ); - await visitOperatorMode({ - submode: 'code', - fileView: 'browser', - codePath: `${testDestinationRealmURL}index`, - }); - let outerFolder = await verifyFolderWithUUIDInFileTree( - assert, - listingName, - ); - - let instanceFolder = `${outerFolder}Author/`; - await openDir(assert, instanceFolder); - await verifyJSONWithUUIDInFolder(assert, instanceFolder); - }); - }); - module('"install"', function () { - test('card listing', async function (assert) { - const listingName = 'author'; - - await executeCommand( - ListingInstallCommand, - authorListingId, - testDestinationRealmURL, - ); - await visitOperatorMode({ - submode: 'code', - fileView: 'browser', - codePath: `${testDestinationRealmURL}index`, - }); - - let outerFolder = await verifyFolderWithUUIDInFileTree( - assert, - listingName, - ); - let gtsFilePath = `${outerFolder}${listingName}/author.gts`; - await openDir(assert, gtsFilePath); - await verifyFileInFileTree(assert, gtsFilePath); - let examplePath = `${outerFolder}${listingName}/Author/example.json`; - await openDir(assert, examplePath); - await verifyFileInFileTree(assert, examplePath); - }); - - test('listing installs relationships of examples and its modules', async function (assert) { - const listingName = 'blog-post'; - - await executeCommand( - ListingInstallCommand, - blogPostListingId, - testDestinationRealmURL, - ); - await visitOperatorMode({ - submode: 'code', - fileView: 'browser', - codePath: `${testDestinationRealmURL}index`, - }); - - let outerFolder = await verifyFolderWithUUIDInFileTree( - assert, - listingName, - ); - let blogPostModulePath = `${outerFolder}blog-post/blog-post.gts`; - let authorModulePath = `${outerFolder}author/author.gts`; - await openDir(assert, blogPostModulePath); - await verifyFileInFileTree(assert, blogPostModulePath); - await openDir(assert, authorModulePath); - await verifyFileInFileTree(assert, authorModulePath); - - let blogPostExamplePath = `${outerFolder}blog-post/BlogPost/example.json`; - let authorExamplePath = `${outerFolder}author/Author/example.json`; - let authorCompanyExamplePath = `${outerFolder}author/AuthorCompany/example.json`; - await openDir(assert, blogPostExamplePath); - await verifyFileInFileTree(assert, blogPostExamplePath); - await openDir(assert, authorExamplePath); - await verifyFileInFileTree(assert, authorExamplePath); - await openDir(assert, authorCompanyExamplePath); - await verifyFileInFileTree(assert, authorCompanyExamplePath); - }); - - test('field listing', async function (assert) { - const listingName = 'contact-link'; - const contactLinkFieldListingCardId = `${mockCatalogURL}FieldListing/contact-link`; - - await executeCommand( - ListingInstallCommand, - contactLinkFieldListingCardId, - testDestinationRealmURL, - ); - - await visitOperatorMode({ - submode: 'code', - fileView: 'browser', - codePath: `${testDestinationRealmURL}index`, - }); - - // contact-link-[uuid]/ - let outerFolder = await verifyFolderWithUUIDInFileTree( - assert, - listingName, - ); - await openDir(assert, `${outerFolder}fields/contact-link.gts`); - let gtsFilePath = `${outerFolder}fields/contact-link.gts`; - await verifyFileInFileTree(assert, gtsFilePath); - }); - - test('skill listing', async function (assert) { - const listingName = 'pirate-skill'; - const listingId = `${mockCatalogURL}SkillListing/${listingName}`; - await executeCommand( - ListingInstallCommand, - listingId, - testDestinationRealmURL, - ); - await visitOperatorMode({ - submode: 'code', - fileView: 'browser', - codePath: `${testDestinationRealmURL}index`, - }); - - let outerFolder = await verifyFolderWithUUIDInFileTree( - assert, - listingName, - ); - let instancePath = `${outerFolder}Skill/pirate-speak.json`; - await openDir(assert, instancePath); - await verifyFileInFileTree(assert, instancePath); - }); - }); - module('"remix"', function () { - test('card listing: installs the card and redirects to code mode with persisted playground selection for first example successfully', async function (assert) { - const listingName = 'author'; - const listingId = `${mockCatalogURL}Listing/${listingName}`; - await visitOperatorMode({ - stacks: [[]], - }); - await executeCommand( - ListingRemixCommand, - listingId, - testDestinationRealmURL, - ); - await settled(); - await verifySubmode(assert, 'code'); - await toggleFileTree(); - let outerFolder = await verifyFolderWithUUIDInFileTree( - assert, - listingName, - ); - let instanceFile = `${outerFolder}${listingName}/Author/example.json`; - await openDir(assert, instanceFile); - await verifyFileInFileTree(assert, instanceFile); - let gtsFilePath = `${outerFolder}${listingName}/author.gts`; - await openDir(assert, gtsFilePath); - await verifyFileInFileTree(assert, gtsFilePath); - await settled(); - assert - .dom( - '[data-test-playground-panel] [data-test-boxel-card-header-title]', - ) - .hasText('Author - Mike Dane'); - }); - test('skill listing: installs the card and redirects to code mode with preview on first skill successfully', async function (assert) { - const listingName = 'pirate-skill'; - const listingId = `${mockCatalogURL}SkillListing/${listingName}`; - await executeCommand( - ListingRemixCommand, - listingId, - testDestinationRealmURL, - ); - await settled(); - await verifySubmode(assert, 'code'); - await toggleFileTree(); - let outerFolder = await verifyFolderWithUUIDInFileTree( - assert, - listingName, - ); - let instancePath = `${outerFolder}Skill/pirate-speak.json`; - await openDir(assert, instancePath); - await verifyFileInFileTree(assert, instancePath); - let cardId = - testDestinationRealmURL + instancePath.replace('.json', ''); - await waitFor('[data-test-card-resource-loaded]'); - assert - .dom(`[data-test-code-mode-card-renderer-header="${cardId}"]`) - .exists(); - }); - test('theme listing: installs the theme example and redirects to code mode successfully', async function (assert) { - const listingName = 'cardstack-theme'; - await executeCommand( - ListingRemixCommand, - themeListingId, - testDestinationRealmURL, - ); - await settled(); - await verifySubmode(assert, 'code'); - await toggleFileTree(); - let outerFolder = await verifyFolderWithUUIDInFileTree( - assert, - listingName, - ); - let instancePath = `${outerFolder}theme/theme-example.json`; - await openDir(assert, instancePath); - await verifyFileInFileTree(assert, instancePath); - let cardId = - testDestinationRealmURL + instancePath.replace('.json', ''); - await waitFor('[data-test-card-resource-loaded]'); - assert - .dom(`[data-test-code-mode-card-renderer-header="${cardId}"]`) - .exists(); - }); - }); - - skip('"use" is successful even if target realm does not have a trailing slash', async function (assert) { - const listingName = 'author'; - const listingId = mockCatalogURL + 'Listing/author.json'; - await executeCommand( - ListingUseCommand, - listingId, - removeTrailingSlash(testDestinationRealmURL), - ); - await visitOperatorMode({ - submode: 'code', - fileView: 'browser', - codePath: `${testDestinationRealmURL}index`, - }); - let outerFolder = await verifyFolderWithUUIDInFileTree( - assert, - listingName, - ); - - let instanceFolder = `${outerFolder}Author`; - await openDir(assert, instanceFolder); - await verifyJSONWithUUIDInFolder(assert, instanceFolder); - }); - - test('"install" is successful even if target realm does not have a trailing slash', async function (assert) { - const listingName = 'author'; - await executeCommand( - ListingInstallCommand, - authorListingId, - removeTrailingSlash(testDestinationRealmURL), - ); - await visitOperatorMode({ - submode: 'code', - fileView: 'browser', - codePath: `${testDestinationRealmURL}index`, - }); - - let outerFolder = await verifyFolderWithUUIDInFileTree( - assert, - listingName, - ); - - let gtsFilePath = `${outerFolder}${listingName}/author.gts`; - await openDir(assert, gtsFilePath); - await verifyFileInFileTree(assert, gtsFilePath); - let instancePath = `${outerFolder}${listingName}/Author/example.json`; - - await openDir(assert, instancePath); - await verifyFileInFileTree(assert, instancePath); - }); - - test('"remix" is successful even if target realm does not have a trailing slash', async function (assert) { - const listingName = 'author'; - const listingId = `${mockCatalogURL}Listing/${listingName}`; - await visitOperatorMode({ - stacks: [[]], - }); - await executeCommand( - ListingRemixCommand, - listingId, - removeTrailingSlash(testDestinationRealmURL), - ); - await settled(); - await verifySubmode(assert, 'code'); - await toggleFileTree(); - let outerFolder = await verifyFolderWithUUIDInFileTree( - assert, - listingName, - ); - let instancePath = `${outerFolder}${listingName}/Author/example.json`; - await openDir(assert, instancePath); - await verifyFileInFileTree(assert, instancePath); - let gtsFilePath = `${outerFolder}${listingName}/author.gts`; - await openDir(assert, gtsFilePath); - await verifyFileInFileTree(assert, gtsFilePath); - await settled(); - assert - .dom('[data-test-playground-panel] [data-test-boxel-card-header-title]') - .hasText('Author - Mike Dane'); - }); - }); -}); - -function removeTrailingSlash(url: string): string { - return url.endsWith('/') && url.length > 1 ? url.slice(0, -1) : url; -} diff --git a/packages/host/tests/acceptance/real-catalog-app-test.gts b/packages/host/tests/acceptance/real-catalog-app-test.gts deleted file mode 100644 index 03130bfb3a4..00000000000 --- a/packages/host/tests/acceptance/real-catalog-app-test.gts +++ /dev/null @@ -1,67 +0,0 @@ -import { getOwner } from '@ember/owner'; -import { visit, waitFor, waitUntil } from '@ember/test-helpers'; - -import { getService } from '@universal-ember/test-support'; -import { module, skip } from 'qunit'; - -import { ensureTrailingSlash } from '@cardstack/runtime-common'; - -import ENV from '@cardstack/host/config/environment'; -import HostModeService from '@cardstack/host/services/host-mode-service'; - -import { setupLocalIndexing } from '../helpers'; -import { setupApplicationTest } from '../helpers/setup'; - -const catalogRealmURL = ensureTrailingSlash(ENV.resolvedCatalogRealmURL); -const CATALOG_READINESS_URL = `${catalogRealmURL}_readiness-check?acceptHeader=application%2Fvnd.api%2Bjson`; - -class StubHostModeService extends HostModeService { - override get isActive() { - return true; - } - - override get hostModeOrigin() { - return 'http://localhost:4201'; - } -} - -module('Acceptance | Catalog | real catalog app', function (hooks) { - setupApplicationTest(hooks); - setupLocalIndexing(hooks); - - hooks.beforeEach(function () { - getOwner(this)!.register('service:host-mode-service', StubHostModeService); - }); - - // CS-9919 - Skipping this test for now as the catalog realm is now setup only in - // part for speed in host tests. - skip('visiting /catalog/ renders the catalog index card', async function (assert) { - let realmServer = getService('realm-server'); - await realmServer.ready; - await ensureCatalogRealmReady(); - - await visit('/catalog/'); - - await waitFor('[data-test-catalog-app]', { timeout: 30_000 }); - assert.dom('[data-test-card-error]').doesNotExist(); - assert.dom('[data-test-catalog-app]').exists(); - }); -}); - -async function ensureCatalogRealmReady() { - let network = getService('network'); - await waitUntil( - async () => { - try { - let response = await network.fetch(CATALOG_READINESS_URL); - return response.ok; - } catch (e) { - return false; - } - }, - { - timeout: 30_000, - timeoutMessage: `Timed out waiting for catalog realm readiness at ${CATALOG_READINESS_URL}`, - }, - ); -} diff --git a/packages/host/tests/integration/audio-fields-test.gts b/packages/host/tests/integration/audio-fields-test.gts deleted file mode 100644 index ebca94fba65..00000000000 --- a/packages/host/tests/integration/audio-fields-test.gts +++ /dev/null @@ -1,484 +0,0 @@ -import { getService } from '@universal-ember/test-support'; -import { module, test } from 'qunit'; - -import { ensureTrailingSlash } from '@cardstack/runtime-common'; -import type { Loader } from '@cardstack/runtime-common/loader'; - -import ENV from '@cardstack/host/config/environment'; - -import { - setupBaseRealm, - field, - contains, - CardDef, - Component, -} from '../helpers/base-realm'; -import { renderCard } from '../helpers/render-component'; -import { setupRenderingTest } from '../helpers/setup'; - -type FieldFormat = 'embedded' | 'atom' | 'edit' | 'fitted'; - -module('Integration | audio fields', function (hooks) { - setupRenderingTest(hooks); - setupBaseRealm(hooks); - - let loader: Loader; - let catalogRealmURL = ensureTrailingSlash(ENV.resolvedCatalogRealmURL); - - let AudioFieldClass: any; - - let catalogFieldsLoaded = false; - - hooks.beforeEach(async function () { - loader = getService('loader-service').loader; - if (!catalogFieldsLoaded) { - await loadCatalogFields(); - catalogFieldsLoaded = true; - } - }); - - async function loadCatalogFields() { - const audioModule: any = await loader.import( - `${catalogRealmURL}fields/audio`, - ); - AudioFieldClass = audioModule.default; - } - - async function renderField( - FieldClass: any, - value: unknown, - format: FieldFormat = 'embedded', - ) { - const fieldFormat = format; - const fieldType = FieldClass; - - class TestCard extends CardDef { - @field sample = contains(fieldType); - - static isolated = class Isolated extends Component { - format: FieldFormat = fieldFormat; - - - }; - } - - let card = new TestCard({ sample: value }); - await renderCard(loader, card, 'isolated'); - } - - async function renderConfiguredField( - FieldClass: any, - value: unknown, - configuration: Record = {}, - ) { - const fieldType = FieldClass; - - class TestCard extends CardDef { - @field sample = contains(fieldType, { configuration }); - - static isolated = class Isolated extends Component { - - }; - } - - let card = new TestCard({ sample: value }); - await renderCard(loader, card, 'isolated'); - } - - function buildField(FieldClass: any, attrs: Record) { - return new FieldClass(attrs); - } - - // Sample audio data for tests - const sampleAudioData = { - url: 'http://localhost:4201/does-not-exist/audio/sample.mp3', - filename: 'sample.mp3', - mimeType: 'audio/mpeg', - duration: 180, // 3 minutes - fileSize: 3145728, // 3MB - cardTitle: 'Test Track', - artist: 'Test Artist', - }; - - const minimalAudioData = { - url: 'http://localhost:4201/does-not-exist/audio/minimal.mp3', - filename: 'minimal.mp3', - }; - - // ============================================ - // Basic Rendering Tests - // ============================================ - - test('audio field renders embedded view with valid data', async function (assert) { - await renderField( - AudioFieldClass, - buildField(AudioFieldClass, sampleAudioData), - ); - - assert.dom('[data-test-audio-embedded]').exists('embedded view renders'); - assert - .dom('[data-test-audio-title]') - .hasText('Test Track', 'title is displayed'); - assert - .dom('[data-test-audio-artist]') - .hasText('Test Artist', 'artist is displayed'); - assert.dom('[data-test-audio-play-btn]').exists('play button is rendered'); - }); - - test('audio field renders atom view with valid data', async function (assert) { - await renderField( - AudioFieldClass, - buildField(AudioFieldClass, sampleAudioData), - 'atom', - ); - - assert.dom('[data-test-audio-atom]').exists('atom view renders'); - assert - .dom('[data-test-audio-atom]') - .hasTextContaining('Test Track', 'displays title in atom view'); - }); - - test('audio field renders fitted view with valid data', async function (assert) { - await renderField( - AudioFieldClass, - buildField(AudioFieldClass, sampleAudioData), - 'fitted', - ); - - assert.dom('[data-test-audio-fitted]').exists('fitted view renders'); - }); - - test('audio field renders edit view with valid data', async function (assert) { - await renderField( - AudioFieldClass, - buildField(AudioFieldClass, sampleAudioData), - 'edit', - ); - - assert.dom('[data-test-audio-edit]').exists('edit view renders'); - assert - .dom('[data-test-audio-uploaded-file]') - .exists('shows uploaded file info'); - }); - - // ============================================ - // Empty State Tests - // ============================================ - - test('missing audio renders placeholder in embedded view', async function (assert) { - await renderField(AudioFieldClass, buildField(AudioFieldClass, {})); - - assert - .dom('[data-test-audio-placeholder]') - .exists('placeholder is displayed'); - assert - .dom('[data-test-audio-placeholder]') - .hasTextContaining('No audio file', 'placeholder text is shown'); - }); - - test('missing audio renders placeholder in fitted view', async function (assert) { - await renderField( - AudioFieldClass, - buildField(AudioFieldClass, {}), - 'fitted', - ); - - assert - .dom('[data-test-audio-fitted-placeholder]') - .exists('fitted placeholder is displayed'); - assert - .dom('[data-test-audio-fitted-placeholder]') - .hasTextContaining('No audio', 'fitted placeholder text is shown'); - }); - - test('missing audio shows upload area in edit view', async function (assert) { - await renderField(AudioFieldClass, buildField(AudioFieldClass, {}), 'edit'); - - assert.dom('[data-test-audio-edit]').exists('edit view renders'); - assert - .dom('[data-test-audio-upload-area]') - .exists('upload area is displayed'); - }); - - test('undefined audio field renders placeholder', async function (assert) { - await renderField(AudioFieldClass, undefined); - - assert - .dom('[data-test-audio-placeholder]') - .exists('placeholder is displayed for undefined'); - }); - - // ============================================ - // Computed Field Tests - // ============================================ - - test('displayTitle shows title when available', async function (assert) { - await renderField( - AudioFieldClass, - buildField(AudioFieldClass, { - ...sampleAudioData, - cardTitle: 'Custom Title', - }), - ); - - assert - .dom('[data-test-audio-title]') - .hasText('Custom Title', 'custom title is displayed'); - }); - - test('displayTitle falls back to filename when no title', async function (assert) { - await renderField( - AudioFieldClass, - buildField(AudioFieldClass, { - url: 'http://localhost:4201/does-not-exist/audio.mp3', - filename: 'my-song.mp3', - }), - ); - - assert - .dom('[data-test-audio-title]') - .hasText('my-song.mp3', 'filename is displayed as fallback'); - }); - - test('displayTitle falls back to default when no title or filename', async function (assert) { - await renderField( - AudioFieldClass, - buildField(AudioFieldClass, { - url: 'http://localhost:4201/does-not-exist/audio.mp3', - }), - ); - - assert - .dom('[data-test-audio-title]') - .hasText('Untitled Audio', 'default title is displayed'); - }); - - // ============================================ - // Presentation Style Tests - // ============================================ - - test('waveform-player presentation renders correctly', async function (assert) { - await renderConfiguredField( - AudioFieldClass, - buildField(AudioFieldClass, sampleAudioData), - { presentation: 'waveform-player' }, - ); - - assert - .dom('[data-test-waveform-player]') - .exists('waveform player is rendered'); - }); - - test('mini-player presentation renders correctly', async function (assert) { - await renderConfiguredField( - AudioFieldClass, - buildField(AudioFieldClass, sampleAudioData), - { presentation: 'mini-player' }, - ); - - assert.dom('[data-test-mini-player]').exists('mini player is rendered'); - }); - - test('album-cover presentation renders correctly', async function (assert) { - await renderConfiguredField( - AudioFieldClass, - buildField(AudioFieldClass, sampleAudioData), - { presentation: 'album-cover' }, - ); - - assert - .dom('[data-test-album-cover-player]') - .exists('album cover player is rendered'); - }); - - test('trim-editor presentation renders correctly', async function (assert) { - await renderConfiguredField( - AudioFieldClass, - buildField(AudioFieldClass, sampleAudioData), - { presentation: 'trim-editor' }, - ); - - assert.dom('[data-test-trim-editor]').exists('trim editor is rendered'); - }); - - test('playlist-row presentation renders correctly', async function (assert) { - await renderConfiguredField( - AudioFieldClass, - buildField(AudioFieldClass, sampleAudioData), - { presentation: 'playlist-row' }, - ); - - assert.dom('[data-test-playlist-row]').exists('playlist row is rendered'); - }); - - test('inline-player (default) presentation renders correctly', async function (assert) { - await renderConfiguredField( - AudioFieldClass, - buildField(AudioFieldClass, sampleAudioData), - {}, // Default presentation - ); - - assert - .dom('[data-test-audio-embedded]') - .exists('inline player is rendered (default)'); - }); - - // ============================================ - // Configuration Options Tests - // ============================================ - - test('showVolume option renders volume control', async function (assert) { - await renderConfiguredField( - AudioFieldClass, - buildField(AudioFieldClass, sampleAudioData), - { options: { showVolume: true } }, - ); - - assert - .dom('[data-test-volume-control]') - .exists('volume control is rendered'); - }); - - test('showSpeedControl option renders speed selector', async function (assert) { - await renderConfiguredField( - AudioFieldClass, - buildField(AudioFieldClass, sampleAudioData), - { options: { showSpeedControl: true } }, - ); - - assert - .dom('[data-test-audio-advanced-controls]') - .exists('advanced controls are rendered'); - assert - .dom('[data-test-audio-speed-control]') - .exists('speed control is rendered'); - }); - - test('showLoopControl option renders loop checkbox', async function (assert) { - await renderConfiguredField( - AudioFieldClass, - buildField(AudioFieldClass, sampleAudioData), - { options: { showLoopControl: true } }, - ); - - assert - .dom('[data-test-audio-loop-control]') - .exists('loop control is rendered'); - assert - .dom('[data-test-audio-loop-checkbox]') - .exists('loop checkbox is rendered'); - }); - - // ============================================ - // Metadata Display Tests - // ============================================ - - test('audio metadata displays correctly', async function (assert) { - await renderField( - AudioFieldClass, - buildField(AudioFieldClass, sampleAudioData), - ); - - assert.dom('[data-test-audio-metadata]').exists('metadata section exists'); - }); - - test('minimal audio data still renders', async function (assert) { - await renderField( - AudioFieldClass, - buildField(AudioFieldClass, minimalAudioData), - ); - - assert.dom('[data-test-audio-embedded]').exists('player still renders'); - assert.dom('[data-test-audio-artist]').doesNotExist('no artist shown'); - }); - - // ============================================ - // Player Controls Tests - // ============================================ - - test('play button exists and is clickable', async function (assert) { - await renderField( - AudioFieldClass, - buildField(AudioFieldClass, sampleAudioData), - ); - - assert.dom('[data-test-audio-play-btn]').exists('play button exists'); - }); - - test('seek bar is hidden when audio has not loaded metadata', async function (assert) { - await renderField( - AudioFieldClass, - buildField(AudioFieldClass, sampleAudioData), - ); - - // The seek bar/controls only appear after audio metadata is loaded - // With fake URLs, the audio never loads, so controls won't appear - // This is expected behavior - controls are conditional on audioDuration - assert - .dom('[data-test-audio-controls]') - .doesNotExist('controls hidden until audio loads'); - assert.dom('[data-test-audio-play-btn]').exists('play button always shows'); - }); - - // ============================================ - // Atom View Tests - // ============================================ - - test('atom view shows audio icon', async function (assert) { - await renderField( - AudioFieldClass, - buildField(AudioFieldClass, sampleAudioData), - 'atom', - ); - - assert.dom('[data-test-audio-atom] svg').exists('audio icon is shown'); - assert - .dom('[data-test-audio-atom]') - .hasTextContaining('Test Track', 'title is shown'); - }); - - test('atom view shows displayTitle fallback', async function (assert) { - await renderField( - AudioFieldClass, - buildField(AudioFieldClass, { url: 'test.mp3' }), - 'atom', - ); - - assert - .dom('[data-test-audio-atom]') - .hasTextContaining('Untitled Audio', 'fallback title shown'); - }); - - // ============================================ - // Edit View Tests - // ============================================ - - test('edit view shows metadata fields when audio is uploaded', async function (assert) { - await renderField( - AudioFieldClass, - buildField(AudioFieldClass, sampleAudioData), - 'edit', - ); - - assert.dom('[data-test-audio-edit]').exists('edit view renders'); - assert - .dom('[data-test-audio-uploaded-file]') - .exists('uploaded file info shown'); - }); - - test('edit view shows upload prompt when no audio', async function (assert) { - await renderField(AudioFieldClass, buildField(AudioFieldClass, {}), 'edit'); - - assert - .dom('[data-test-audio-upload-area]') - .exists('upload prompt is shown'); - }); -}); diff --git a/packages/host/tests/integration/color-field-configuration-test.gts b/packages/host/tests/integration/color-field-configuration-test.gts deleted file mode 100644 index 842603a4150..00000000000 --- a/packages/host/tests/integration/color-field-configuration-test.gts +++ /dev/null @@ -1,425 +0,0 @@ -import { settled } from '@ember/test-helpers'; - -import { getService } from '@universal-ember/test-support'; - -import { module, test } from 'qunit'; - -import { ensureTrailingSlash } from '@cardstack/runtime-common'; - -import type { Loader } from '@cardstack/runtime-common/loader'; - -import ENV from '@cardstack/host/config/environment'; - -import { - setupBaseRealm, - field, - contains, - CardDef, - Component, -} from '../helpers/base-realm'; -import { renderCard } from '../helpers/render-component'; -import { setupRenderingTest } from '../helpers/setup'; - -let loader: Loader; - -module('Integration | color field configuration', function (hooks) { - setupRenderingTest(hooks); - setupBaseRealm(hooks); - - let catalogRealmURL = ensureTrailingSlash(ENV.resolvedCatalogRealmURL); - let CatalogColorFieldClass: any; - - hooks.beforeEach(async function () { - loader = getService('loader-service').loader; - const colorModule: any = await loader.import( - `${catalogRealmURL}fields/color`, - ); - CatalogColorFieldClass = colorModule.default; - }); - - async function renderConfiguredField( - value: string | null, - configuration: any, - ) { - class TestCard extends CardDef { - @field sample = contains(CatalogColorFieldClass, { configuration }); - - static isolated = class Isolated extends Component { - - }; - } - - let card = new TestCard({ sample: value }); - await renderCard(loader, card, 'isolated'); - } - - // ============================================ - // Valid Variant Configuration Tests - // ============================================ - - test('standard variant renders color picker', async function (assert) { - await renderConfiguredField('#3b82f6', { variant: 'standard' }); - - assert - .dom('[data-test-field-container] .color-picker') - .exists('standard variant renders color picker'); - }); - - test('swatches-picker variant renders color palette', async function (assert) { - await renderConfiguredField('#3b82f6', { variant: 'swatches-picker' }); - - assert - .dom('[data-test-field-container] .color-palette-group') - .exists('swatches-picker variant renders color palette'); - }); - - test('slider variant renders slider controls', async function (assert) { - await renderConfiguredField('#3b82f6', { variant: 'slider' }); - - assert - .dom('[data-test-field-container] .slider-controls-editor') - .exists('slider variant renders slider controls'); - }); - - test('advanced variant renders advanced editor', async function (assert) { - await renderConfiguredField('#3b82f6', { variant: 'advanced' }); - - assert - .dom('[data-test-field-container] .advanced-color-editor') - .exists('advanced variant renders advanced editor'); - }); - - test('wheel variant renders color wheel', async function (assert) { - await renderConfiguredField('#3b82f6', { variant: 'wheel' }); - - assert - .dom('[data-test-field-container] .color-wheel-editor') - .exists('wheel variant renders color wheel'); - }); - - test('missing variant defaults to standard', async function (assert) { - await renderConfiguredField('#3b82f6', {}); - - assert - .dom('[data-test-field-container] .color-picker') - .exists('missing variant defaults to standard'); - }); - - // ============================================ - // Invalid Variant Configuration Tests - // ============================================ - - test('invalid variant value falls back to standard variant', async function (assert) { - await renderConfiguredField('#3b82f6', { variant: 'not-a-real-variant' }); - - assert - .dom('[data-test-field-container] .color-picker') - .exists('invalid variant falls back to standard'); - assert - .dom('[data-test-field-container] .advanced-color-editor') - .doesNotExist('advanced variant is not rendered'); - }); - - test('null variant value defaults to standard variant', async function (assert) { - await renderConfiguredField('#3b82f6', { variant: null }); - - assert - .dom('[data-test-field-container] .color-picker') - .exists('null variant defaults to standard'); - }); - - // ============================================ - // Valid Options Configuration Tests - // ============================================ - - test('showRecent option displays recent colors addon', async function (assert) { - await renderConfiguredField('#3b82f6', { - variant: 'standard', - options: { showRecent: true }, - }); - - assert - .dom('[data-test-field-container] .recent-colors-addon') - .exists('showRecent option displays recent colors addon'); - }); - - test('showContrastChecker option displays contrast checker addon', async function (assert) { - await renderConfiguredField('#3b82f6', { - variant: 'standard', - options: { showContrastChecker: true }, - }); - - assert - .dom('[data-test-field-container] .contrast-checker-addon') - .exists('showContrastChecker option displays contrast checker addon'); - }); - - test('showRecent defaults to false', async function (assert) { - await renderConfiguredField('#3b82f6', { variant: 'standard' }); - - assert - .dom('[data-test-field-container] .recent-colors-addon') - .doesNotExist('showRecent defaults to false'); - }); - - test('showContrastChecker defaults to false', async function (assert) { - await renderConfiguredField('#3b82f6', { variant: 'standard' }); - - assert - .dom('[data-test-field-container] .contrast-checker-addon') - .doesNotExist('showContrastChecker defaults to false'); - }); - - test('maxRecentHistory option is respected', async function (assert) { - await renderConfiguredField('#3b82f6', { - variant: 'standard', - options: { - showRecent: true, - maxRecentHistory: 5, - }, - }); - - assert - .dom('[data-test-field-container] .recent-colors-addon') - .exists('recent colors addon is shown with custom maxRecentHistory'); - }); - - test('swatches-picker variant supports paletteColors option', async function (assert) { - await renderConfiguredField('#3b82f6', { - variant: 'swatches-picker', - options: { - paletteColors: ['#ff0000', '#00ff00', '#0000ff'], - }, - }); - - assert - .dom('[data-test-field-container] .color-palette-group') - .exists('swatches-picker variant renders with palette colors'); - }); - - test('base options are ignored when variant is advanced', async function (assert) { - await renderConfiguredField('#3b82f6', { - variant: 'advanced', - options: { - showRecent: true, - showContrastChecker: true, - maxRecentHistory: 5, - }, - }); - - assert - .dom('[data-test-field-container] .advanced-color-editor') - .exists('advanced variant renders'); - assert - .dom('[data-test-field-container] .recent-colors-addon') - .doesNotExist('base options are ignored for advanced variant'); - assert - .dom('[data-test-field-container] .contrast-checker-addon') - .doesNotExist('base options are ignored for advanced variant'); - }); - - // ============================================ - // Invalid Options Configuration Tests - // ============================================ - - test('invalid option values are handled gracefully', async function (assert) { - await renderConfiguredField('#3b82f6', { - variant: 'standard', - options: { - showRecent: true, - maxRecentHistory: 'ten' as any, // invalid type - unknownProperty: 'should be ignored', - } as any, - }); - - assert - .dom('[data-test-field-container] .color-picker') - .exists('renders with invalid option values'); - assert - .dom('[data-test-field-container] .recent-colors-addon') - .exists('addon still renders despite invalid maxRecentHistory'); - }); - - test('null or undefined options are handled', async function (assert) { - await renderConfiguredField('#3b82f6', { - variant: 'standard', - options: null, - }); - - assert - .dom('[data-test-field-container] .color-picker') - .exists('renders with null options'); - }); - - // ============================================ - // Valid Format Configuration Tests - // ============================================ - - test('advanced variant renders with configured default format', async function (assert) { - await renderConfiguredField('rgb(59, 130, 246)', { - variant: 'advanced', - options: { defaultFormat: 'rgb' }, - }); - - await settled(); - - assert - .dom('[data-test-field-container] .advanced-color-editor') - .exists('advanced variant renders'); - assert - .dom('[data-test-field-container] .color-value-input') - .exists('RGB input section is visible'); - }); - - test('advanced variant parses CSS color names', async function (assert) { - await renderConfiguredField('blue', { - variant: 'advanced', - options: { defaultFormat: 'hex' }, - }); - - await settled(); - - assert - .dom('[data-test-field-container] .advanced-color-editor') - .exists('CSS color name is parsed and component renders'); - }); - - test('advanced variant parses RGB values', async function (assert) { - await renderConfiguredField('rgb(255, 0, 0)', { - variant: 'advanced', - options: { defaultFormat: 'hsl' }, - }); - - await settled(); - - assert - .dom('[data-test-field-container] .advanced-color-editor') - .exists('RGB value is parsed and component renders'); - }); - - test('advanced variant parses hex values', async function (assert) { - await renderConfiguredField('#ff0000', { - variant: 'advanced', - options: { defaultFormat: 'rgb' }, - }); - - await settled(); - - assert - .dom('[data-test-field-container] .advanced-color-editor') - .exists('hex value is parsed and component renders'); - }); - - test('slider variant displays HSL format when configured', async function (assert) { - await renderConfiguredField('#3b82f6', { - variant: 'slider', - options: { defaultFormat: 'hsl' }, - }); - - await settled(); - - const hslSliders = document.querySelectorAll( - '[data-test-field-container] .slider-label.hue, [data-test-field-container] .slider-label.saturation, [data-test-field-container] .slider-label.lightness', - ); - - assert.ok( - hslSliders.length >= 3, - 'slider variant with HSL format displays HSL sliders', - ); - }); - - test('slider variant displays RGB format when configured', async function (assert) { - await renderConfiguredField('#3b82f6', { - variant: 'slider', - options: { defaultFormat: 'rgb' }, - }); - - await settled(); - - const rgbSliders = document.querySelectorAll( - '[data-test-field-container] .slider-label.red, [data-test-field-container] .slider-label.green, [data-test-field-container] .slider-label.blue', - ); - - assert.ok( - rgbSliders.length >= 3, - 'slider variant with RGB format displays RGB sliders', - ); - }); - - test('wheel variant displays with configured format', async function (assert) { - await renderConfiguredField('#3b82f6', { - variant: 'wheel', - options: { defaultFormat: 'rgb' }, - }); - - await settled(); - - assert - .dom('[data-test-field-container] .color-wheel-editor') - .exists('wheel variant renders with RGB format'); - }); - - // ============================================ - // Invalid Format Configuration Tests - // ============================================ - - test('invalid defaultFormat falls back to default', async function (assert) { - await renderConfiguredField('#3b82f6', { - variant: 'advanced', - options: { defaultFormat: 'invalid-format' as any }, - }); - - await settled(); - - assert - .dom('[data-test-field-container] .advanced-color-editor') - .exists('editor renders with invalid defaultFormat'); - }); - - // ============================================ - // Invalid Color Value Handling Tests - // ============================================ - - test('invalid color values are handled gracefully', async function (assert) { - const invalidColors = [ - null, - '', - ' ', - 'not-a-color', - '#gggggg', - 'rgb(999, 999, 999)', - '#ff', - ]; - - for (const color of invalidColors) { - await renderConfiguredField(color, { variant: 'standard' }); - - assert - .dom('[data-test-field-container] .color-picker') - .exists(`color value "${color}" renders without error`); - } - }); - - // ============================================ - // Edge Cases and Boundary Conditions - // ============================================ - - test('multiple invalid values are handled gracefully', async function (assert) { - await renderConfiguredField('invalid-color', { - variant: 'not-a-variant', - options: { - showRecent: 'yes' as any, - maxRecentHistory: 'ten' as any, - unknownProperty: 'ignored', - } as any, - }); - - assert - .dom('[data-test-field-container] .color-picker') - .exists('renders despite multiple invalid values'); - }); -}); diff --git a/packages/host/tests/integration/commands/listing-update-specs-test.gts b/packages/host/tests/integration/commands/listing-update-specs-test.gts deleted file mode 100644 index 2491ea26da7..00000000000 --- a/packages/host/tests/integration/commands/listing-update-specs-test.gts +++ /dev/null @@ -1,185 +0,0 @@ -import { getService } from '@universal-ember/test-support'; -import { module, test } from 'qunit'; - -import { - baseRealm, - SupportedMimeType, - type Realm, -} from '@cardstack/runtime-common'; -import type { Loader } from '@cardstack/runtime-common/loader'; - -import ListingUpdateSpecsCommand from '@cardstack/host/commands/listing-update-specs'; - -import type { CardDef } from 'https://cardstack.com/base/card-api'; - -import { - setupCardLogs, - setupIntegrationTestRealm, - setupLocalIndexing, - setupRealmServerEndpoints, - testRealmURL, -} from '../../helpers'; -import { setupBaseRealm } from '../../helpers/base-realm'; -import { setupMockMatrix } from '../../helpers/mock-matrix'; -import { setupRenderingTest } from '../../helpers/setup'; - -import type { Listing } from '@cardstack/catalog/listing/listing'; - -module('Integration | commands | listing-update-specs', function (hooks) { - setupRenderingTest(hooks); - setupBaseRealm(hooks); - setupLocalIndexing(hooks); - - const modulePath = `${testRealmURL}listing-example.gts`; - const exampleCardId = `${testRealmURL}Example/1`; - let loader: Loader; - let testRealm: Realm; - - setupRealmServerEndpoints(hooks, [ - { - route: '_dependencies', - getResponse: async function () { - return new Response( - JSON.stringify({ - data: [ - { - type: 'dependencies', - id: exampleCardId, - attributes: { - canonicalUrl: exampleCardId, - realmUrl: testRealmURL, - entryType: 'instance', - hasError: false, - dependencies: [modulePath], - }, - }, - ], - }), - { - status: 200, - headers: { 'Content-Type': SupportedMimeType.JSONAPI }, - }, - ); - }, - }, - { - route: '_request-forward', - getResponse: async function (req: Request) { - const body = await req.json(); - if (body.url === 'https://openrouter.ai/api/v1/chat/completions') { - return new Response( - JSON.stringify({ - choices: [ - { - message: { - content: 'stubbed readme', - }, - }, - ], - }), - { status: 200, headers: { 'Content-Type': 'application/json' } }, - ); - } - return new Response(JSON.stringify({ error: 'Unknown endpoint' }), { - status: 404, - headers: { 'Content-Type': 'application/json' }, - }); - }, - }, - ]); - - setupCardLogs( - hooks, - async () => await loader.import(`${baseRealm.url}card-api`), - ); - - let mockMatrixUtils = setupMockMatrix(hooks); - - hooks.beforeEach(function () { - loader = getService('loader-service').loader; - }); - - hooks.beforeEach(async function () { - ({ realm: testRealm } = await setupIntegrationTestRealm({ - mockMatrixUtils, - realmURL: testRealmURL, - contents: { - 'listing-example.gts': ` - import { CardDef } from 'https://cardstack.com/base/card-api'; - - export class ExampleCard extends CardDef { - static displayName = 'Example Card'; - } - `, - 'Example/1.json': { - data: { - id: `${testRealmURL}Example/1`, - type: 'card', - meta: { - adoptsFrom: { - module: '../listing-example', - name: 'ExampleCard', - }, - }, - }, - }, - }, - })); - await getService('realm').login(testRealmURL); - }); - - test('updates specs when a module adds a new export', async function (assert) { - let commandService = getService('command-service'); - let listingUpdateSpecsCommand = new ListingUpdateSpecsCommand( - commandService.commandContext, - ); - - let store = getService('store'); - - let ListingClass = ( - (await loader.import( - '@cardstack/catalog/catalog-app/listing/listing', - )) as { - Listing: typeof Listing; - } - ).Listing; - let listing = (await store.add(new ListingClass(), { - realm: testRealmURL, - })) as InstanceType; - let exampleCard = (await store.get(`${testRealmURL}Example/1`)) as CardDef; - (listing as Listing).examples = [exampleCard]; - - let result = await listingUpdateSpecsCommand.execute({ listing }); - let specNames = result.specs.map((spec) => spec.ref?.name).filter(Boolean); - assert.deepEqual(specNames, ['ExampleCard'], 'initial spec is created'); - - await testRealm.write( - 'listing-example.gts', - ` - import { CardDef } from 'https://cardstack.com/base/card-api'; - - export class ExampleCard extends CardDef { - static displayName = 'Example Card'; - } - - export class AnotherCard extends CardDef { - static displayName = 'Another Card'; - } - `, - ); - getService('loader-service').resetLoader({ - reason: 'refresh module exports', - }); - - result = await listingUpdateSpecsCommand.execute({ listing }); - specNames = result.specs - .map((spec) => spec.ref?.name) - .filter(Boolean) - .sort(); - assert.deepEqual( - specNames, - ['AnotherCard', 'ExampleCard'], - 'new export is reflected in specs', - ); - }); -}); diff --git a/packages/host/tests/integration/commands/set-user-system-card-test.gts b/packages/host/tests/integration/commands/set-user-system-card-test.gts deleted file mode 100644 index efd865f7332..00000000000 --- a/packages/host/tests/integration/commands/set-user-system-card-test.gts +++ /dev/null @@ -1,63 +0,0 @@ -import { getOwner } from '@ember/owner'; -import type { RenderingTestContext } from '@ember/test-helpers'; - -import { getService } from '@universal-ember/test-support'; -import { module, test } from 'qunit'; - -import SetUserSystemCardCommand from '@cardstack/host/commands/set-user-system-card'; -import RealmService from '@cardstack/host/services/realm'; - -import { - setupIntegrationTestRealm, - setupLocalIndexing, - testRealmInfo, - testRealmURL, -} from '../../helpers'; - -import { setupMockMatrix } from '../../helpers/mock-matrix'; -import { setupRenderingTest } from '../../helpers/setup'; - -class StubRealmService extends RealmService { - get defaultReadableRealm() { - return { - path: testRealmURL, - info: testRealmInfo, - }; - } -} - -module('Integration | commands | set-user-system-card', function (hooks) { - setupRenderingTest(hooks); - setupLocalIndexing(hooks); - - let mockMatrixUtils = setupMockMatrix(hooks, { - loggedInAs: '@testuser:localhost', - activeRealms: [testRealmURL], - }); - - hooks.beforeEach(function (this: RenderingTestContext) { - getOwner(this)!.register('service:realm', StubRealmService); - }); - - hooks.beforeEach(async function () { - await setupIntegrationTestRealm({ - mockMatrixUtils, - contents: {}, - }); - }); - - test('sets the system card account data', async function (assert) { - let commandService = getService('command-service'); - let command = new SetUserSystemCardCommand(commandService.commandContext); - - let systemCardId = 'http://localhost:4201/catalog/SystemCard/default'; - - await command.execute({ - cardId: systemCardId, - }); - - assert.deepEqual(mockMatrixUtils.getSystemCardAccountData(), { - id: systemCardId, - }); - }); -}); diff --git a/packages/host/tests/integration/commands/upload-image-test.gts b/packages/host/tests/integration/commands/upload-image-test.gts deleted file mode 100644 index b6f51ff6747..00000000000 --- a/packages/host/tests/integration/commands/upload-image-test.gts +++ /dev/null @@ -1,389 +0,0 @@ -import { getOwner } from '@ember/owner'; -import type { RenderingTestContext } from '@ember/test-helpers'; - -import { getService } from '@universal-ember/test-support'; -import { module, test } from 'qunit'; - -import type NetworkService from '@cardstack/host/services/network'; -import RealmService from '@cardstack/host/services/realm'; - -import { - setupIntegrationTestRealm, - setupLocalIndexing, - setupRealmServerEndpoints, - testRealmInfo, - testRealmURL, - SYSTEM_CARD_FIXTURE_CONTENTS, -} from '../../helpers'; -import { setupBaseRealm } from '../../helpers/base-realm'; -import { setupMockMatrix } from '../../helpers/mock-matrix'; -import { setupRenderingTest } from '../../helpers/setup'; - -import type UploadImageCommand from '@cardstack/catalog/commands/upload-image'; - -class StubRealmService extends RealmService { - get defaultReadableRealm() { - return { - path: testRealmURL, - info: testRealmInfo, - }; - } -} - -module('Integration | commands | upload-image', function (hooks) { - setupRenderingTest(hooks); - setupBaseRealm(hooks); - setupLocalIndexing(hooks); - - let mockMatrixUtils = setupMockMatrix(hooks, { - loggedInAs: '@testuser:localhost', - activeRealms: [testRealmURL], - autostart: true, - }); - - let lastForwardPayload: any; - let forwardPayloads: any[] = []; - let lastDirectUploadRequest: - | { - url: string; - formData: FormData; - } - | undefined; - let networkService: NetworkService; - let directUploadFetchHandler: - | ((request: Request) => Promise) - | undefined; - - const directUploadResponse = { - success: true, - errors: [], - result: { - id: 'direct-upload-id', - uploadURL: 'https://upload.imagedelivery.net/direct-upload-url', - }, - }; - - setupRealmServerEndpoints(hooks, [ - { - route: '_request-forward', - getResponse: async (request: Request) => { - const body = await request.json(); - lastForwardPayload = body; - forwardPayloads.push(body); - - let responsePayload; - if ( - body.url === - 'https://api.cloudflare.com/client/v4/accounts/4a94a1eb2d21bbbe160234438a49f687/images/v2/direct_upload' - ) { - responsePayload = directUploadResponse; - } else { - responsePayload = { - success: true, - errors: [], - result: { - id: 'cloudflare-image-id', - }, - }; - } - - return new Response(JSON.stringify(responsePayload), { - status: 200, - headers: { 'Content-Type': 'application/json' }, - }); - }, - }, - ]); - - hooks.beforeEach(async function (this: RenderingTestContext) { - getOwner(this)!.register('service:realm', StubRealmService); - lastForwardPayload = undefined; - forwardPayloads = []; - lastDirectUploadRequest = undefined; - directUploadFetchHandler = async (request: Request) => { - if ( - request.url === 'https://upload.imagedelivery.net/direct-upload-url' - ) { - const formData = await request.formData(); - lastDirectUploadRequest = { - url: request.url, - formData, - }; - return new Response( - JSON.stringify({ - success: true, - errors: [], - result: { - id: 'cloudflare-direct-upload-id', - }, - }), - { - status: 200, - headers: { 'Content-Type': 'application/json' }, - }, - ); - } - return null; - }; - networkService = getService('network'); - networkService.virtualNetwork.mount(directUploadFetchHandler, { - prepend: true, - }); - - await setupIntegrationTestRealm({ - mockMatrixUtils, - contents: { - ...SYSTEM_CARD_FIXTURE_CONTENTS, - }, - }); - }); - - hooks.afterEach(function () { - if (directUploadFetchHandler) { - networkService.virtualNetwork.unmount(directUploadFetchHandler); - directUploadFetchHandler = undefined; - } - }); - - test('uploads image via Cloudflare and saves CloudflareImage card', async function (assert) { - assert.expect(7); - - const commandService = getService('command-service'); - const loaderService = getService('loader-service'); - const loader = loaderService.loader; - const UploadImageCommandClass: typeof UploadImageCommand = ( - (await loader.import('@cardstack/catalog/commands/upload-image')) as { - default: typeof UploadImageCommand; - } - ).default; - const command = new UploadImageCommandClass(commandService.commandContext); - - const result = await command.execute({ - sourceImageUrl: 'https://example.com/photo.jpg', - targetRealmUrl: testRealmURL, - }); - - assert.ok(result, 'command returns a result card'); - assert.ok(result.cardId, 'result card contains a card id'); - - assert.strictEqual( - lastForwardPayload.url, - 'https://api.cloudflare.com/client/v4/accounts/4a94a1eb2d21bbbe160234438a49f687/images/v1', - 'requests are forwarded to Cloudflare upload endpoint', - ); - assert.true(lastForwardPayload.multipart, 'request is sent as multipart'); - - const store = getService('store'); - const savedCard = store.peek(result.cardId!); - assert.ok(savedCard, 'saved card can be retrieved from the store'); - assert.strictEqual( - (savedCard as any).cloudflareId, - 'cloudflare-image-id', - 'saved card has expected Cloudflare id', - ); - assert.strictEqual( - (savedCard as any).url, - 'https://i.boxel.site/cloudflare-image-id/public', - 'computed URL uses expected Cloudflare delivery format with Boxel domain', - ); - }); - - test('performs direct upload when source is a blob URL', async function (assert) { - assert.expect(12); - - const commandService = getService('command-service'); - const loaderService = getService('loader-service'); - const loader = loaderService.loader; - const UploadImageCommandClass: typeof UploadImageCommand = ( - (await loader.import('@cardstack/catalog/commands/upload-image')) as { - default: typeof UploadImageCommand; - } - ).default; - const command = new UploadImageCommandClass(commandService.commandContext); - - const fileBlob = new Blob(['fake image bytes'], { type: 'image/png' }); - const file = - typeof File === 'function' - ? new File([fileBlob], 'photo.png', { type: 'image/png' }) - : (Object.assign(fileBlob, { name: 'photo.png' }) as Blob & { - name: string; - }); - const objectUrl = URL.createObjectURL(file); - - const originalFetch = globalThis.fetch; - globalThis.fetch = async ( - input: RequestInfo | URL, - init?: RequestInit, - ): Promise => { - let url: string; - if (typeof input === 'string') { - url = input; - } else if (input instanceof URL) { - url = input.href; - } else if (input instanceof Request) { - url = input.url; - } else { - url = String(input); - } - - if (url.startsWith('blob:')) { - return new Response(file, { - status: 200, - headers: { - 'Content-Type': file.type, - }, - }); - } - - return originalFetch(input as RequestInfo, init); - }; - - try { - const result = await command.execute({ - sourceImageUrl: objectUrl, - targetRealmUrl: testRealmURL, - }); - - assert.ok(result, 'command returns a result'); - assert.ok(result.cardId, 'result card contains an id'); - - assert.strictEqual( - forwardPayloads.length, - 1, - 'proxy invoked only for direct upload URL request', - ); - - const [directUploadCall] = forwardPayloads; - assert.strictEqual( - directUploadCall.url, - 'https://api.cloudflare.com/client/v4/accounts/4a94a1eb2d21bbbe160234438a49f687/images/v2/direct_upload', - 'first proxy call requests direct upload URL', - ); - assert.true( - directUploadCall.multipart, - 'direct upload URL request is sent as multipart form-data', - ); - - assert.ok(lastDirectUploadRequest, 'direct upload performed via fetch'); - assert.strictEqual( - lastDirectUploadRequest?.url, - 'https://upload.imagedelivery.net/direct-upload-url', - 'direct upload posts to provided upload URL', - ); - - const fileEntry = lastDirectUploadRequest?.formData.get('file'); - assert.ok(fileEntry, 'form data includes file field'); - const uploadedFile = fileEntry as Blob; - assert.strictEqual(uploadedFile.type, 'image/png'); - - const directUploadPayload = JSON.parse(directUploadCall.requestBody); - assert.false( - directUploadPayload.requireSignedURLs, - 'direct upload request specifies default signing behaviour', - ); - - const expectedBytes = new Uint8Array(await file.arrayBuffer()); - const uploadedBytes = new Uint8Array(await uploadedFile.arrayBuffer()); - assert.deepEqual( - Array.from(uploadedBytes), - Array.from(expectedBytes), - 'uploaded file contents match source blob', - ); - - const store = getService('store'); - const savedCard = store.peek(result.cardId!); - assert.strictEqual( - (savedCard as any).cloudflareId, - 'cloudflare-direct-upload-id', - 'saved card uses id returned by direct upload', - ); - } finally { - globalThis.fetch = originalFetch; - URL.revokeObjectURL(objectUrl); - } - }); - - test('performs direct upload when source is a data URI', async function (assert) { - assert.expect(12); - - const commandService = getService('command-service'); - const loaderService = getService('loader-service'); - const loader = loaderService.loader; - const UploadImageCommandClass: typeof UploadImageCommand = ( - (await loader.import('@cardstack/catalog/commands/upload-image')) as { - default: typeof UploadImageCommand; - } - ).default; - const command = new UploadImageCommandClass(commandService.commandContext); - - const payloadString = 'fake image bytes'; - const nodeBuffer = (globalThis as any).Buffer as - | { - from( - input: string, - encoding?: string, - ): { toString(encoding: string): string }; - } - | undefined; - const base64Payload = nodeBuffer - ? nodeBuffer.from(payloadString, 'utf-8').toString('base64') - : btoa(payloadString); - const dataUri = `data:image/png;base64,${base64Payload}`; - - const result = await command.execute({ - sourceImageUrl: dataUri, - targetRealmUrl: testRealmURL, - }); - - assert.ok(result, 'command returns a result'); - assert.ok(result.cardId, 'result card contains an id'); - - assert.strictEqual( - forwardPayloads.length, - 1, - 'proxy invoked only for direct upload URL request', - ); - - const [directUploadCall] = forwardPayloads; - assert.strictEqual( - directUploadCall.url, - 'https://api.cloudflare.com/client/v4/accounts/4a94a1eb2d21bbbe160234438a49f687/images/v2/direct_upload', - 'proxy call requests direct upload URL', - ); - assert.true( - directUploadCall.multipart, - 'direct upload URL request is sent as multipart form-data', - ); - - assert.ok(lastDirectUploadRequest, 'direct upload performed via fetch'); - assert.strictEqual( - lastDirectUploadRequest?.url, - 'https://upload.imagedelivery.net/direct-upload-url', - 'direct upload posts to provided upload URL', - ); - - const fileEntry = lastDirectUploadRequest?.formData.get('file'); - assert.ok(fileEntry, 'form data includes file field'); - const uploadedFile = fileEntry as File; - assert.strictEqual(uploadedFile.type, 'image/png'); - assert.strictEqual(uploadedFile.name, 'upload.png'); - - const expectedBytes = nodeBuffer - ? new Uint8Array(nodeBuffer.from(payloadString, 'utf-8') as any) - : Uint8Array.from(payloadString, (char) => char.charCodeAt(0)); - const uploadedBytes = new Uint8Array(await uploadedFile.arrayBuffer()); - assert.deepEqual( - Array.from(uploadedBytes), - Array.from(expectedBytes), - 'uploaded file contents match data URI payload', - ); - - const store = getService('store'); - const savedCard = store.peek(result.cardId!); - assert.strictEqual( - (savedCard as any).cloudflareId, - 'cloudflare-direct-upload-id', - 'saved card uses id returned by direct upload', - ); - }); -}); diff --git a/packages/host/tests/integration/date-time-fields-test.gts b/packages/host/tests/integration/date-time-fields-test.gts deleted file mode 100644 index 3598044c3b9..00000000000 --- a/packages/host/tests/integration/date-time-fields-test.gts +++ /dev/null @@ -1,1039 +0,0 @@ -import { click } from '@ember/test-helpers'; - -import { getService } from '@universal-ember/test-support'; -import { module, test } from 'qunit'; - -import { ensureTrailingSlash } from '@cardstack/runtime-common'; -import type { Loader } from '@cardstack/runtime-common/loader'; - -import ENV from '@cardstack/host/config/environment'; - -import { - setupBaseRealm, - field, - contains, - CardDef, - Component, -} from '../helpers/base-realm'; -import { renderCard } from '../helpers/render-component'; -import { setupRenderingTest } from '../helpers/setup'; - -type FieldFormat = 'embedded' | 'atom' | 'edit'; - -module('Integration | date-time fields', function (hooks) { - setupRenderingTest(hooks); - setupBaseRealm(hooks); - - let loader: Loader; - let catalogRealmURL = ensureTrailingSlash(ENV.resolvedCatalogRealmURL); - - let DateFieldClass: any; - let TimeFieldClass: any; - let DatetimeFieldClass: any; - let DatetimeStampFieldClass: any; - let DayFieldClass: any; - let DateRangeFieldClass: any; - let TimeRangeFieldClass: any; - let DurationFieldClass: any; - let RelativeTimeFieldClass: any; - let TimePeriodFieldClass: any; - let MonthDayFieldClass: any; - let YearFieldClass: any; - let MonthFieldClass: any; - let MonthYearFieldClass: any; - let WeekFieldClass: any; - let QuarterFieldClass: any; - let RecurringPatternFieldClass: any; - - let catalogFieldsLoaded = false; - - hooks.beforeEach(async function () { - loader = getService('loader-service').loader; - if (!catalogFieldsLoaded) { - await loadCatalogFields(); - catalogFieldsLoaded = true; - } - }); - - async function loadCatalogFields() { - const dateModule: any = await loader.import( - `${catalogRealmURL}fields/date`, - ); - DateFieldClass = dateModule.default; - - const timeModule: any = await loader.import( - `${catalogRealmURL}fields/time`, - ); - TimeFieldClass = timeModule.default; - - const datetimeModule: any = await loader.import( - `${catalogRealmURL}fields/date-time`, - ); - DatetimeFieldClass = datetimeModule.default; - - const datetimeStampModule: any = await loader.import( - `${catalogRealmURL}fields/datetime-stamp`, - ); - DatetimeStampFieldClass = datetimeStampModule.default; - - const dayModule: any = await loader.import( - `${catalogRealmURL}fields/date/day`, - ); - DayFieldClass = dayModule.default; - - const dateRangeModule: any = await loader.import( - `${catalogRealmURL}fields/date/date-range`, - ); - DateRangeFieldClass = dateRangeModule.default; - - const timeRangeModule: any = await loader.import( - `${catalogRealmURL}fields/time/time-range`, - ); - TimeRangeFieldClass = timeRangeModule.default; - - const durationModule: any = await loader.import( - `${catalogRealmURL}fields/time/duration`, - ); - DurationFieldClass = durationModule.default; - - const relativeModule: any = await loader.import( - `${catalogRealmURL}fields/time/relative-time`, - ); - RelativeTimeFieldClass = relativeModule.default; - - const timePeriodModule: any = await loader.import( - `${catalogRealmURL}fields/time-period`, - ); - TimePeriodFieldClass = timePeriodModule.default; - - const monthDayModule: any = await loader.import( - `${catalogRealmURL}fields/date/month-day`, - ); - MonthDayFieldClass = monthDayModule.default; - - const yearModule: any = await loader.import( - `${catalogRealmURL}fields/date/year`, - ); - YearFieldClass = yearModule.default; - - const monthModule: any = await loader.import( - `${catalogRealmURL}fields/date/month`, - ); - MonthFieldClass = monthModule.default; - - const monthYearModule: any = await loader.import( - `${catalogRealmURL}fields/date/month-year`, - ); - MonthYearFieldClass = monthYearModule.default; - - const weekModule: any = await loader.import( - `${catalogRealmURL}fields/date/week`, - ); - WeekFieldClass = weekModule.default; - - const quarterModule: any = await loader.import( - `${catalogRealmURL}fields/date/quarter`, - ); - QuarterFieldClass = quarterModule.default; - - const recurringModule: any = await loader.import( - `${catalogRealmURL}fields/recurring-pattern`, - ); - RecurringPatternFieldClass = recurringModule.default; - } - - async function renderField( - FieldClass: any, - value: unknown, - format: FieldFormat = 'embedded', - ) { - const fieldFormat = format; - const fieldType = FieldClass; - - class TestCard extends CardDef { - @field sample = contains(fieldType); - - static isolated = class Isolated extends Component { - format: FieldFormat = fieldFormat; - - - }; - } - - let card = new TestCard({ sample: value }); - await renderCard(loader, card, 'isolated'); - } - - async function renderConfiguredField( - FieldClass: any, - value: unknown, - presentation: any, - extraConfig: Record = {}, - ) { - const fieldType = FieldClass; - const configuration = { presentation, ...extraConfig } as Record< - any, - unknown - >; - - class TestCard extends CardDef { - @field sample = contains(fieldType, { configuration }); - - static isolated = class Isolated extends Component { - - }; - } - - let card = new TestCard({ sample: value }); - await renderCard(loader, card, 'isolated'); - } - - function buildField(FieldClass: any, attrs: Record) { - return new FieldClass(attrs); - } - - test('core date & time fields render their embedded views', async function (assert) { - await renderField(DateFieldClass, '2024-05-01'); - assert.dom('[data-test-date-embedded]').exists(); - assert - .dom('[data-test-date-embedded]') - .doesNotContainText('No date set', 'date value is displayed'); - - await renderField( - TimeFieldClass, - buildField(TimeFieldClass, { value: '14:00' }), - ); - assert - .dom('[data-test-time-embedded]') - .hasTextContaining('2:00 PM', 'time value is formatted to 12-hour clock'); - - await renderField(DatetimeFieldClass, '2024-05-01T14:30:00'); - assert.dom('[data-test-datetime-embedded]').exists(); - assert - .dom('[data-test-datetime-embedded]') - .doesNotContainText('No date/time set', 'datetime value is displayed'); - }); - - test('range, duration, and relative fields show formatted summaries', async function (assert) { - await renderField( - DateRangeFieldClass, - buildField(DateRangeFieldClass, { - start: '2024-05-01', - end: '2024-05-10', - }), - ); - assert - .dom('[data-test-date-range-embedded]') - .hasText('May 1, 2024 → May 10, 2024'); - - await renderField( - TimeRangeFieldClass, - buildField(TimeRangeFieldClass, { - start: buildField(TimeFieldClass, { value: '09:00' }), - end: buildField(TimeFieldClass, { value: '17:00' }), - }), - ); - assert.dom('[data-test-time-range-embedded]').hasText('09:00 → 17:00'); - - await renderField( - DurationFieldClass, - buildField(DurationFieldClass, { - hours: 1, - minutes: 30, - seconds: 0, - }), - ); - assert - .dom('[data-test-duration-embedded]') - .hasText('1h 30m', 'duration renders in compact notation'); - - await renderField( - RelativeTimeFieldClass, - buildField(RelativeTimeFieldClass, { amount: 3, unit: 'hours' }), - ); - assert.dom('[data-test-relative-time-embedded]').hasText('In 3 hours'); - - await renderField( - RecurringPatternFieldClass, - buildField(RecurringPatternFieldClass, { - pattern: 'weekly', - startDate: '2024-05-01', - endDate: '2024-06-01', - }), - ); - assert - .dom('[data-test-recurring-embedded]') - .hasTextContaining( - 'Weekly', - 'recurring pattern summarizes the configured schedule', - ); - }); - - test('partial calendar fields render friendly labels', async function (assert) { - await renderField( - MonthDayFieldClass, - buildField(MonthDayFieldClass, { month: '05', day: '15' }), - ); - assert - .dom('[data-test-month-day-embedded]') - .hasText('May 15', 'month/day is spelled out'); - - await renderField( - YearFieldClass, - buildField(YearFieldClass, { value: 2025 }), - ); - assert.dom('[data-test-year-embedded]').hasText('2025'); - - await renderField( - MonthFieldClass, - buildField(MonthFieldClass, { value: 5 }), - ); - assert.dom('[data-test-month-embedded]').hasText('May'); - - await renderField( - MonthYearFieldClass, - buildField(MonthYearFieldClass, { value: '2025-05' }), - ); - assert.dom('[data-test-month-year-embedded]').hasText('May 2025'); - - await renderField( - WeekFieldClass, - buildField(WeekFieldClass, { value: '2025-W20' }), - ); - assert.dom('[data-test-week-embedded]').hasText('week 20, 2025'); - - await renderField( - QuarterFieldClass, - buildField(QuarterFieldClass, { quarter: 2, year: 2025 }), - ); - assert.dom('[data-test-quarter-embedded]').hasText('Q2 2025'); - }); - - test('presentation modes render their specialized components', async function (assert) { - // DatetimeField presentations - await renderConfiguredField( - DatetimeFieldClass, - '2025-01-01T00:00:00Z', - 'countdown', - { countdownOptions: { label: 'Launch', showControls: true } }, - ); - assert.dom('[data-test-countdown]').exists(); - - await renderConfiguredField( - DatetimeFieldClass, - '2024-01-01T00:00:00Z', - 'timeAgo', - { - timeAgoOptions: { eventLabel: 'Last Activity', updateInterval: 60000 }, - }, - ); - assert.dom('[data-test-relative-time]').exists(); - - await renderConfiguredField( - DatetimeFieldClass, - '2024-06-01T10:00:00Z', - 'timeline', - { timelineOptions: { eventName: 'Order Placed', status: 'complete' } }, - ); - assert.dom('[data-test-timeline-event]').exists(); - - await renderConfiguredField( - DatetimeFieldClass, - '2025-06-01T10:00:00Z', - 'expirationWarning', - { expirationOptions: { itemName: 'API Token' } }, - ); - assert.dom('[data-test-expiration-warning]').exists(); - - // DateField presentation: age - await renderConfiguredField(DateFieldClass, '1990-05-01', 'age', { - ageOptions: { showNextBirthday: true }, - }); - assert.dom('[data-test-age-calculator]').exists(); - - // DateRangeField presentation: businessDays (needs instance) - await renderConfiguredField( - DateRangeFieldClass, - buildField(DateRangeFieldClass, { - start: '2024-05-01', - end: '2024-05-10', - }), - 'businessDays', - ); - assert.dom('[data-test-business-days]').exists(); - - // TimeField presentation: timeSlots (needs instance) - await renderConfiguredField( - TimeFieldClass, - buildField(TimeFieldClass, { - value: '09:00', - }), - 'timeSlots', - { timeSlotsOptions: { availableSlots: ['09:00', '10:00'] } }, - ); - assert.dom('[data-test-time-slots]').exists(); - }); - - test('missing values render placeholders in embedded and atom modes', async function (assert) { - await renderField(DateFieldClass, undefined); - assert.dom('[data-test-date-embedded]').hasText('No date set'); - - await renderField(DateFieldClass, undefined, 'atom'); - assert.dom('[data-test-date-atom]').hasTextContaining('No date'); - - await renderField(TimeFieldClass, buildField(TimeFieldClass, {})); - assert.dom('[data-test-time-embedded]').hasText('No time set'); - - await renderField(TimeFieldClass, buildField(TimeFieldClass, {}), 'atom'); - assert.dom('[data-test-time-atom]').hasTextContaining('No time'); - - await renderField(DatetimeFieldClass, undefined); - assert - .dom('[data-test-datetime-embedded]') - .hasTextContaining('No date/time set'); - - await renderField(DatetimeFieldClass, undefined, 'atom'); - assert.dom('[data-test-datetime-atom]').hasTextContaining('No date/time'); - }); - - test('datetime supports custom format and invalid fallback', async function (assert) { - await renderConfiguredField( - DatetimeFieldClass, - '2024-05-01T14:30:00', - 'standard', - { format: 'YYYY-MM-DD HH:mm' }, - ); - assert - .dom('[data-test-datetime-embedded]') - .hasTextContaining('2024-05-01 14:30'); - - await renderConfiguredField(DatetimeFieldClass, 'not-a-date', 'standard'); - assert.dom('[data-test-datetime-embedded]').hasTextContaining('Invalid'); - }); - - test('date field supports preset and custom format', async function (assert) { - await renderConfiguredField(DateFieldClass, '2024-05-01', 'standard', { - format: 'YYYY/MM/DD', - }); - assert.dom('[data-test-date-embedded]').hasText('2024/05/01'); - - await renderConfiguredField(DateFieldClass, '2024-05-01', 'standard', { - preset: 'short', - }); - assert.dom('[data-test-date-embedded]').hasText('5/1/24'); - }); - - test('time formatting respects hourCycle/timeStyle options', async function (assert) { - await renderConfiguredField( - TimeFieldClass, - buildField(TimeFieldClass, { value: '14:00' }), - 'standard', - { hourCycle: 'h24', timeStyle: 'short' }, - ); - assert.dom('[data-test-time-embedded]').hasTextContaining('14'); - assert.dom('[data-test-time-embedded]').doesNotContainText('PM'); - }); - - test('open-ended ranges render friendly phrases (date/time ranges)', async function (assert) { - await renderField( - DateRangeFieldClass, - buildField(DateRangeFieldClass, { start: '2024-05-01' }), - ); - assert.dom('[data-test-date-range-embedded]').hasText('From May 1, 2024'); - - await renderField( - DateRangeFieldClass, - buildField(DateRangeFieldClass, { end: '2024-05-10' }), - ); - assert.dom('[data-test-date-range-embedded]').hasText('Until May 10, 2024'); - - await renderField(DateRangeFieldClass, buildField(DateRangeFieldClass, {})); - assert.dom('[data-test-date-range-embedded]').hasText('No date range set'); - - await renderField( - TimeRangeFieldClass, - buildField(TimeRangeFieldClass, { - start: buildField(TimeFieldClass, { value: '09:00' }), - }), - ); - assert.dom('[data-test-time-range-embedded]').hasText('From 09:00'); - - await renderField( - TimeRangeFieldClass, - buildField(TimeRangeFieldClass, { - end: buildField(TimeFieldClass, { value: '17:00' }), - }), - ); - assert.dom('[data-test-time-range-embedded]').hasText('Until 17:00'); - - await renderField(TimeRangeFieldClass, buildField(TimeRangeFieldClass, {})); - assert.dom('[data-test-time-range-embedded]').hasText('No time range set'); - }); - - test('atom mode renders compact badges', async function (assert) { - await renderField( - DateRangeFieldClass, - buildField(DateRangeFieldClass, { - start: '2024-05-01', - end: '2024-05-10', - }), - 'atom', - ); - assert - .dom('[data-test-date-range-atom]') - .hasTextContaining('5/1/24 - 5/10/24'); - - await renderField( - DurationFieldClass, - buildField(DurationFieldClass, { - hours: 1, - minutes: 5, - seconds: 0, - }), - 'atom', - ); - assert.dom('[data-test-duration-atom]').hasText('1h 5m'); - - await renderField( - MonthYearFieldClass, - buildField(MonthYearFieldClass, { - value: '2025-05', - }), - 'atom', - ); - assert.dom('[data-test-month-year-atom]').hasTextContaining('May 2025'); - - await renderField(WeekFieldClass, buildField(WeekFieldClass, {}), 'atom'); - assert.dom('[data-test-week-atom]').hasTextContaining('No week'); - - await renderField(MonthFieldClass, buildField(MonthFieldClass, {}), 'atom'); - assert.dom('[data-test-month-atom]').hasTextContaining('No month'); - - await renderField(YearFieldClass, buildField(YearFieldClass, {}), 'atom'); - assert.dom('[data-test-year-atom]').hasTextContaining('No year'); - - await renderField( - MonthDayFieldClass, - buildField(MonthDayFieldClass, {}), - 'atom', - ); - assert.dom('[data-test-month-day-atom]').hasTextContaining('No date'); - }); - - test('edit mode interactions: time range duration and duration validation', async function (assert) { - await renderField( - TimeRangeFieldClass, - buildField(TimeRangeFieldClass, { - start: buildField(TimeFieldClass, { value: '09:00' }), - end: buildField(TimeFieldClass, { value: '10:00' }), - }), - 'edit', - ); - assert.dom('[data-test-time-input]').exists({ count: 2 }); - assert - .dom('[data-test-field-container]') - .hasTextContaining('Duration: 1 hours'); - - await renderField( - DurationFieldClass, - buildField(DurationFieldClass, { hours: 1, minutes: 30, seconds: 0 }), - 'edit', - ); - assert.dom('[data-test-field-container]').hasTextContaining('Hours'); - assert.dom('[data-test-field-container]').hasTextContaining('Minutes'); - assert.dom('[data-test-field-container]').hasTextContaining('Seconds'); - }); - - test('edit mode interactions: month/day selects update preview', async function (assert) { - await renderField( - MonthDayFieldClass, - buildField(MonthDayFieldClass, { month: 5, day: 15 }), - 'edit', - ); - assert.dom('[data-test-month-select]').exists(); - assert.dom('[data-test-day-select]').exists(); - }); - - test('presentation content reflects configuration', async function (assert) { - await renderConfiguredField( - DatetimeFieldClass, - '2999-01-01T00:00:00Z', - 'countdown', - { countdownOptions: { label: 'Launch', showControls: true } }, - ); - assert.dom('[data-test-countdown]').exists(); - assert.dom('[data-test-countdown]').hasTextContaining('Launch'); - assert.dom('[data-test-countdown-toggle]').exists(); - assert.dom('[data-test-countdown-reset]').exists(); - - await renderConfiguredField( - DatetimeFieldClass, - '2020-01-01T00:00:00Z', - 'timeAgo', - { timeAgoOptions: { eventLabel: 'Last Activity' } }, - ); - assert.dom('[data-test-relative-time]').exists(); - assert.dom('[data-test-relative-time]').hasTextContaining('Last Activity'); - assert.dom('[data-test-relative-time]').hasTextContaining('ago'); - - await renderConfiguredField( - DatetimeFieldClass, - '2024-06-01T10:00:00Z', - 'timeline', - { timelineOptions: { eventName: 'Order Placed', status: 'complete' } }, - ); - assert.dom('[data-test-timeline-event]').hasTextContaining('Order Placed'); - - await renderConfiguredField( - DatetimeFieldClass, - '2000-01-01T00:00:00Z', - 'expirationWarning', - { expirationOptions: { itemName: 'API Token' } }, - ); - assert.dom('[data-test-expiration-warning]').exists(); - assert.dom('[data-test-expiration-warning]').hasTextContaining('API Token'); - assert.dom('[data-test-expiration-warning]').hasTextContaining('Expired'); - - await renderConfiguredField( - DateRangeFieldClass, - buildField(DateRangeFieldClass, { - start: '2024-05-06', - end: '2024-05-10', - }), - 'businessDays', - ); - assert.dom('[data-test-business-days]').exists(); - assert.dom('[data-test-business-days]').hasTextContaining('Calendar Days:'); - assert.dom('[data-test-business-days]').hasTextContaining('Business Days:'); - - await renderConfiguredField( - TimeFieldClass, - buildField(TimeFieldClass, { value: '09:00' }), - 'timeSlots', - { timeSlotsOptions: { availableSlots: ['09:00 AM', '10:00 AM'] } }, - ); - assert.dom('[data-test-time-slots]').exists(); - await click('[data-test-slot="10:00 AM"]'); - assert - .dom('[data-test-time-slots]') - .hasTextContaining('Selected: 10:00 AM'); - }); - - test('datetime-stamp field renders correctly', async function (assert) { - await renderField(DatetimeStampFieldClass, '2024-05-01T14:30:00Z'); - assert.dom('[data-test-datetime-stamp-embedded]').exists(); - assert - .dom('[data-test-datetime-stamp-embedded]') - .doesNotContainText('No timestamp set', 'timestamp value is displayed'); - - await renderField(DatetimeStampFieldClass, undefined); - assert - .dom('[data-test-datetime-stamp-embedded]') - .hasTextContaining('No timestamp set'); - - await renderField(DatetimeStampFieldClass, '2024-05-01T14:30:00Z', 'atom'); - assert.dom('[data-test-datetime-stamp-atom]').exists(); - assert - .dom('[data-test-datetime-stamp-atom]') - .doesNotContainText('No timestamp'); - }); - - test('day field renders correctly', async function (assert) { - await renderField(DayFieldClass, buildField(DayFieldClass, { value: 15 })); - assert.dom('[data-test-day-embedded]').hasText('15th'); - - await renderField(DayFieldClass, buildField(DayFieldClass, {})); - assert.dom('[data-test-day-embedded]').hasTextContaining('No day set'); - - await renderField( - DayFieldClass, - buildField(DayFieldClass, { value: 15 }), - 'atom', - ); - assert.dom('[data-test-day-atom]').exists(); - assert.dom('[data-test-day-atom]').hasTextContaining('15'); - - await renderField( - DayFieldClass, - buildField(DayFieldClass, { value: 15 }), - 'edit', - ); - assert.dom('[data-test-field-container]').exists(); - }); - - test('time-period field renders correctly', async function (assert) { - await renderField( - TimePeriodFieldClass, - buildField(TimePeriodFieldClass, { - periodLabel: 'Q2 2024', - }), - ); - assert.dom('[data-test-time-period-embedded]').exists(); - assert.dom('[data-test-time-period-embedded]').hasTextContaining('Q2 2024'); - - await renderField( - TimePeriodFieldClass, - buildField(TimePeriodFieldClass, {}), - ); - assert - .dom('[data-test-time-period-embedded]') - .hasTextContaining('No period set'); - - await renderField( - TimePeriodFieldClass, - buildField(TimePeriodFieldClass, { - periodLabel: 'Q2 2024', - }), - 'atom', - ); - assert.dom('[data-test-time-period-atom]').exists(); - assert.dom('[data-test-time-period-atom]').hasTextContaining('Q2 2024'); - }); - - test('time-period field recognizes calendar year format', async function (assert) { - await renderField( - TimePeriodFieldClass, - buildField(TimePeriodFieldClass, { - periodLabel: '2024', - }), - ); - assert - .dom('[data-test-time-period-embedded]') - .hasTextContaining('Calendar Year'); - assert.dom('[data-test-time-period-embedded]').hasTextContaining('2024'); - }); - - test('time-period field recognizes fiscal year format', async function (assert) { - await renderField( - TimePeriodFieldClass, - buildField(TimePeriodFieldClass, { - periodLabel: '2023-2024', - }), - ); - assert - .dom('[data-test-time-period-embedded]') - .hasTextContaining('Fiscal Year'); - assert - .dom('[data-test-time-period-embedded]') - .hasTextContaining('2023-2024'); - - // Short format - await renderField( - TimePeriodFieldClass, - buildField(TimePeriodFieldClass, { - periodLabel: '2023-24', - }), - ); - assert - .dom('[data-test-time-period-embedded]') - .hasTextContaining('Fiscal Year'); - }); - - test('time-period field recognizes quarter format', async function (assert) { - await renderField( - TimePeriodFieldClass, - buildField(TimePeriodFieldClass, { - periodLabel: 'Q1 2024', - }), - ); - assert.dom('[data-test-time-period-embedded]').hasTextContaining('Quarter'); - assert.dom('[data-test-time-period-embedded]').hasTextContaining('Q1 2024'); - - // Reverse format - await renderField( - TimePeriodFieldClass, - buildField(TimePeriodFieldClass, { - periodLabel: '2024 Q3', - }), - ); - assert.dom('[data-test-time-period-embedded]').hasTextContaining('Quarter'); - }); - - test('time-period field recognizes month format', async function (assert) { - await renderField( - TimePeriodFieldClass, - buildField(TimePeriodFieldClass, { - periodLabel: 'January 2024', - }), - ); - assert.dom('[data-test-time-period-embedded]').hasTextContaining('Month'); - - // Abbreviated - await renderField( - TimePeriodFieldClass, - buildField(TimePeriodFieldClass, { - periodLabel: 'Jan 2024', - }), - ); - assert.dom('[data-test-time-period-embedded]').hasTextContaining('Month'); - - // With period - await renderField( - TimePeriodFieldClass, - buildField(TimePeriodFieldClass, { - periodLabel: 'Feb. 2024', - }), - ); - assert.dom('[data-test-time-period-embedded]').hasTextContaining('Month'); - }); - - test('time-period field recognizes week format', async function (assert) { - await renderField( - TimePeriodFieldClass, - buildField(TimePeriodFieldClass, { - periodLabel: 'Week 12 2025', - }), - ); - assert.dom('[data-test-time-period-embedded]').hasTextContaining('Week'); - - // Abbreviated format - await renderField( - TimePeriodFieldClass, - buildField(TimePeriodFieldClass, { - periodLabel: 'Wk12 2025', - }), - ); - assert.dom('[data-test-time-period-embedded]').hasTextContaining('Week'); - - // Reverse format - await renderField( - TimePeriodFieldClass, - buildField(TimePeriodFieldClass, { - periodLabel: '2025 Wk12', - }), - ); - assert.dom('[data-test-time-period-embedded]').hasTextContaining('Week'); - }); - - test('time-period field recognizes session format', async function (assert) { - await renderField( - TimePeriodFieldClass, - buildField(TimePeriodFieldClass, { - periodLabel: 'Fall 2024', - }), - ); - assert.dom('[data-test-time-period-embedded]').hasTextContaining('Session'); - - await renderField( - TimePeriodFieldClass, - buildField(TimePeriodFieldClass, { - periodLabel: 'Spring 2025', - }), - ); - assert.dom('[data-test-time-period-embedded]').hasTextContaining('Session'); - - await renderField( - TimePeriodFieldClass, - buildField(TimePeriodFieldClass, { - periodLabel: 'Summer 2024', - }), - ); - assert.dom('[data-test-time-period-embedded]').hasTextContaining('Session'); - }); - - test('time-period field recognizes session week format', async function (assert) { - await renderField( - TimePeriodFieldClass, - buildField(TimePeriodFieldClass, { - periodLabel: 'Wk4 Spring 2025', - }), - ); - assert - .dom('[data-test-time-period-embedded]') - .hasTextContaining('Session Week'); - }); - - test('time-period field auto-normalizes partial inputs with current year', async function (assert) { - const currentYear = new Date().getFullYear().toString(); - - // Quarter without year - await renderField( - TimePeriodFieldClass, - buildField(TimePeriodFieldClass, { - periodLabel: 'Q1', - }), - ); - assert.dom('[data-test-time-period-embedded]').hasTextContaining('Quarter'); - assert - .dom('[data-test-time-period-embedded]') - .hasTextContaining(currentYear); - - // Month without year - await renderField( - TimePeriodFieldClass, - buildField(TimePeriodFieldClass, { - periodLabel: 'March', - }), - ); - assert.dom('[data-test-time-period-embedded]').hasTextContaining('Month'); - assert - .dom('[data-test-time-period-embedded]') - .hasTextContaining(currentYear); - - // Season without year - await renderField( - TimePeriodFieldClass, - buildField(TimePeriodFieldClass, { - periodLabel: 'Fall', - }), - ); - assert.dom('[data-test-time-period-embedded]').hasTextContaining('Session'); - assert - .dom('[data-test-time-period-embedded]') - .hasTextContaining(currentYear); - - // Week without year - await renderField( - TimePeriodFieldClass, - buildField(TimePeriodFieldClass, { - periodLabel: 'Week 12', - }), - ); - assert.dom('[data-test-time-period-embedded]').hasTextContaining('Week'); - assert - .dom('[data-test-time-period-embedded]') - .hasTextContaining(currentYear); - }); - - test('time-period field displays date range for recognized formats', async function (assert) { - // Quarter shows date range - await renderField( - TimePeriodFieldClass, - buildField(TimePeriodFieldClass, { - periodLabel: 'Q2 2024', - }), - ); - assert.dom('[data-test-time-period-embedded]').hasTextContaining('Apr'); - assert.dom('[data-test-time-period-embedded]').hasTextContaining('Jun'); - - // Month shows date range - await renderField( - TimePeriodFieldClass, - buildField(TimePeriodFieldClass, { - periodLabel: 'May 2024', - }), - ); - assert.dom('[data-test-time-period-embedded]').hasTextContaining('May 1'); - assert.dom('[data-test-time-period-embedded]').hasTextContaining('31'); - }); - - test('time-period field edit mode allows custom input', async function (assert) { - await renderField( - TimePeriodFieldClass, - buildField(TimePeriodFieldClass, { - periodLabel: 'Q3 2024', - }), - 'edit', - ); - assert.dom('[data-test-time-period-input]').exists(); - assert.dom('[data-test-time-period-input]').hasValue('Q3 2024'); - }); - - test('relative time field handles future and past times', async function (assert) { - // Future time - await renderField( - RelativeTimeFieldClass, - buildField(RelativeTimeFieldClass, { amount: 5, unit: 'days' }), - ); - assert.dom('[data-test-relative-time-embedded]').hasText('In 5 days'); - - // Negative amount - await renderField( - RelativeTimeFieldClass, - buildField(RelativeTimeFieldClass, { amount: -3, unit: 'hours' }), - ); - assert.dom('[data-test-relative-time-embedded]').hasText('In -3 hours'); - - // Different units - await renderField( - RelativeTimeFieldClass, - buildField(RelativeTimeFieldClass, { amount: 2, unit: 'weeks' }), - ); - assert.dom('[data-test-relative-time-embedded]').hasText('In 2 weeks'); - - await renderField( - RelativeTimeFieldClass, - buildField(RelativeTimeFieldClass, { amount: 30, unit: 'minutes' }), - ); - assert.dom('[data-test-relative-time-embedded]').hasText('In 30 minutes'); - }); - - test('recurring pattern field displays pattern details', async function (assert) { - // Daily pattern - await renderField( - RecurringPatternFieldClass, - buildField(RecurringPatternFieldClass, { - pattern: 'daily', - startDate: '2024-05-01', - endDate: '2024-05-31', - }), - ); - assert.dom('[data-test-recurring-embedded]').hasTextContaining('Daily'); - - // Monthly pattern - await renderField( - RecurringPatternFieldClass, - buildField(RecurringPatternFieldClass, { - pattern: 'monthly', - startDate: '2024-05-01', - }), - ); - assert.dom('[data-test-recurring-embedded]').hasTextContaining('Monthly'); - - // Custom pattern with interval - await renderField( - RecurringPatternFieldClass, - buildField(RecurringPatternFieldClass, { - pattern: 'custom', - interval: 2, - unit: 'days', - startDate: '2024-05-01', - }), - ); - assert - .dom('[data-test-recurring-embedded]') - .hasTextContaining('Every 2 days'); - }); - - test('edit mode for partial calendar fields renders correctly', async function (assert) { - // Year field edit mode - await renderField( - YearFieldClass, - buildField(YearFieldClass, { value: 2024 }), - 'edit', - ); - assert.dom('[data-test-field-container]').exists(); - - // Month field edit mode - await renderField( - MonthFieldClass, - buildField(MonthFieldClass, { value: 5 }), - 'edit', - ); - assert.dom('[data-test-month-select]').exists(); - - // Quarter field edit mode - await renderField( - QuarterFieldClass, - buildField(QuarterFieldClass, { quarter: 2, year: 2025 }), - 'edit', - ); - assert.dom('[data-test-field-container]').exists(); - - // Week field edit mode - await renderField( - WeekFieldClass, - buildField(WeekFieldClass, { value: '2025-W20' }), - 'edit', - ); - assert.dom('[data-test-field-container]').exists(); - }); -}); diff --git a/packages/host/tests/integration/image-field-configuration-test.gts b/packages/host/tests/integration/image-field-configuration-test.gts deleted file mode 100644 index 51548810735..00000000000 --- a/packages/host/tests/integration/image-field-configuration-test.gts +++ /dev/null @@ -1,270 +0,0 @@ -import { getService } from '@universal-ember/test-support'; -import { module, test } from 'qunit'; - -import { ensureTrailingSlash } from '@cardstack/runtime-common'; -import type { Loader } from '@cardstack/runtime-common/loader'; - -import ENV from '@cardstack/host/config/environment'; - -import { - setupBaseRealm, - field, - contains, - CardDef, - Component, -} from '../helpers/base-realm'; -import { renderCard } from '../helpers/render-component'; -import { setupRenderingTest } from '../helpers/setup'; - -type FieldFormat = 'embedded' | 'atom' | 'edit'; - -let loader: Loader; - -module('Integration | image field configuration', function (hooks) { - setupRenderingTest(hooks); - setupBaseRealm(hooks); - - let catalogRealmURL = ensureTrailingSlash(ENV.resolvedCatalogRealmURL); - let CatalogImageFieldClass: any; - - hooks.beforeEach(async function () { - loader = getService('loader-service').loader; - const imageModule: any = await loader.import( - `${catalogRealmURL}fields/image`, - ); - CatalogImageFieldClass = imageModule.default; - }); - - async function renderConfiguredField( - value: any, - configuration: any, - format: FieldFormat = 'edit', - ) { - const fieldFormat = format; - - class TestCard extends CardDef { - @field sample = contains(CatalogImageFieldClass, { configuration }); - - static isolated = class Isolated extends Component { - format: FieldFormat = fieldFormat; - - - }; - } - - // Create proper ImageField instance - const imageField = - value.url || value.uploadUrl - ? new CatalogImageFieldClass(value) - : new CatalogImageFieldClass(); - - let card = new TestCard({ sample: imageField }); - await renderCard(loader, card, 'isolated'); - } - - // ImageField Variant Tests - test('browse variant renders browse upload component', async function (assert) { - await renderConfiguredField( - {}, - { - variant: 'browse', - }, - 'edit', - ); - - assert - .dom('[data-test-field-container] [data-test-image-field-edit]') - .exists('Image field edit view is rendered'); - - assert - .dom('[data-test-field-container] .browse-upload') - .exists('Browse variant renders browse upload component'); - }); - - test('dropzone variant renders dropzone upload component', async function (assert) { - await renderConfiguredField( - {}, - { - variant: 'dropzone', - }, - 'edit', - ); - - assert - .dom('[data-test-field-container] [data-test-image-field-edit]') - .exists('Image field edit view is rendered'); - - assert - .dom('[data-test-field-container] .dropzone-upload') - .exists('Dropzone variant renders dropzone upload component'); - }); - - test('avatar variant renders avatar upload component', async function (assert) { - await renderConfiguredField( - {}, - { - variant: 'avatar', - }, - 'edit', - ); - - assert - .dom('[data-test-field-container] [data-test-image-field-edit]') - .exists('Image field edit view is rendered'); - - assert - .dom('[data-test-field-container] .avatar-upload') - .exists('Avatar variant renders avatar upload component'); - }); - - test('invalid variant falls back to default browse', async function (assert) { - await renderConfiguredField( - {}, - { - variant: 'invalid-variant', - }, - 'edit', - ); - - // Should fall back to browse variant (default) - assert - .dom('[data-test-field-container] [data-test-image-field-edit]') - .exists('Image field with invalid variant still renders'); - }); - - // ImageField Presentation Tests - test('image presentation renders image presentation component', async function (assert) { - await renderConfiguredField( - { imageUrl: 'https://example.com/image.jpg' }, - { - variant: 'browse', - presentation: 'image', - }, - 'embedded', - ); - - assert - .dom('[data-test-field-container] .image-embedded') - .exists('Image presentation renders image embedded component'); - }); - - test('inline presentation renders inline presentation component', async function (assert) { - await renderConfiguredField( - { imageUrl: 'https://example.com/image.jpg' }, - { - variant: 'browse', - presentation: 'inline', - }, - 'embedded', - ); - - assert - .dom('[data-test-field-container] .image-inline') - .exists('Inline presentation renders inline component'); - }); - - test('card presentation renders card presentation component', async function (assert) { - await renderConfiguredField( - { imageUrl: 'https://example.com/image.jpg' }, - { - variant: 'browse', - presentation: 'card', - }, - 'embedded', - ); - - assert - .dom('[data-test-field-container] .image-card') - .exists('Card presentation renders card component'); - }); - - test('invalid presentation falls back to default image', async function (assert) { - await renderConfiguredField( - { imageUrl: 'https://example.com/image.jpg' }, - { - variant: 'browse', - presentation: 'invalid-presentation', - }, - 'embedded', - ); - - // Should fall back to image presentation (default) - assert - .dom( - '[data-test-field-container] [data-test-image-field-edit], [data-test-field-container] .image-embedded', - ) - .exists('Image field with invalid presentation still renders'); - }); - - // ImageField Options Tests - test('showImageModal option is ignored for avatar variant', async function (assert) { - await renderConfiguredField( - { imageUrl: 'https://example.com/image.jpg' }, - { - variant: 'avatar', - options: { - showImageModal: true, // Should be ignored - }, - }, - 'edit', - ); - - // Avatar variant should not have zoom button (showImageModal is not available) - assert - .dom('[data-test-field-container] [data-test-image-field-edit]') - .exists('Avatar variant renders without showImageModal option'); - - // Verify no zoom button exists (avatar doesn't support showImageModal) - assert - .dom('[data-test-field-container] .zoom-button') - .doesNotExist( - 'Avatar variant does not show zoom button even with showImageModal option', - ); - }); - - test('image field ignores irrelevant config properties', async function (assert) { - await renderConfiguredField( - {}, - { - variant: 'browse', - // These properties should be ignored by image field - maxFiles: 10, - allowReorder: true, - allowBatchSelect: true, - }, - 'edit', - ); - - // Image field should still work normally, ignoring the irrelevant config - assert - .dom('[data-test-field-container] [data-test-image-field-edit]') - .exists( - 'Image field ignores maxFiles, allowReorder, allowBatchSelect configs', - ); - }); - - // Default Config Fallback Tests - test('image field edit view falls back to default browse variant when config is missing', async function (assert) { - await renderConfiguredField({}, {}, 'edit'); - - assert - .dom('[data-test-field-container] [data-test-image-field-edit]') - .exists('Image field edit view renders with default browse variant'); - }); - - test('image field embedded view falls back to default image presentation when config is missing', async function (assert) { - await renderConfiguredField( - { imageUrl: 'https://example.com/image.jpg' }, - {}, - 'embedded', - ); - - assert - .dom('[data-test-field-container] .image-embedded') - .exists('Image field embedded view defaults to image presentation'); - }); -}); diff --git a/packages/host/tests/integration/multiple-image-field-configuration-test.gts b/packages/host/tests/integration/multiple-image-field-configuration-test.gts deleted file mode 100644 index a3d6cd29285..00000000000 --- a/packages/host/tests/integration/multiple-image-field-configuration-test.gts +++ /dev/null @@ -1,316 +0,0 @@ -import { getService } from '@universal-ember/test-support'; -import { module, test } from 'qunit'; - -import { - ensureTrailingSlash, - PermissionsContextName, - type Permissions, -} from '@cardstack/runtime-common'; -import type { Loader } from '@cardstack/runtime-common/loader'; - -import ENV from '@cardstack/host/config/environment'; - -import { provideConsumeContext } from '../helpers'; -import { - setupBaseRealm, - field, - contains, - CardDef, - Component, -} from '../helpers/base-realm'; -import { renderCard } from '../helpers/render-component'; -import { setupRenderingTest } from '../helpers/setup'; - -type FieldFormat = 'embedded' | 'atom' | 'edit'; - -let loader: Loader; - -module('Integration | multiple image field configuration', function (hooks) { - setupRenderingTest(hooks); - setupBaseRealm(hooks); - - let catalogRealmURL = ensureTrailingSlash(ENV.resolvedCatalogRealmURL); - let CatalogMultipleImageFieldClass: any; - let CatalogImageFieldClass: any; - - hooks.beforeEach(async function () { - loader = getService('loader-service').loader; - - const multipleImageModule: any = await loader.import( - `${catalogRealmURL}fields/multiple-image`, - ); - CatalogMultipleImageFieldClass = multipleImageModule.default; - - const imageModule: any = await loader.import( - `${catalogRealmURL}fields/image`, - ); - CatalogImageFieldClass = imageModule.default; - - // Set up permissions to allow editing - const permissions: Permissions = { canWrite: true, canRead: true }; - provideConsumeContext(PermissionsContextName, permissions); - }); - - async function renderConfiguredField( - value: any, - configuration: any, - format: FieldFormat = 'edit', - ) { - const fieldFormat = format; - - class TestCard extends CardDef { - @field sample = contains(CatalogMultipleImageFieldClass, { - configuration, - }); - - static isolated = class Isolated extends Component { - format: FieldFormat = fieldFormat; - - - }; - } - - // Create proper MultipleImageField instance with ImageField instances - const multipleImageField = new CatalogMultipleImageFieldClass(); - if (value.images && Array.isArray(value.images)) { - multipleImageField.images = value.images.map( - (img: any) => new CatalogImageFieldClass(img), - ); - } - - let card = new TestCard({ sample: multipleImageField }); - await renderCard(loader, card, 'isolated'); - } - - // MultipleImageField Variant Tests - test('list variant renders list upload component', async function (assert) { - await renderConfiguredField( - { - images: [{ imageUrl: 'https://example.com/image1.jpg' }], - }, - { - variant: 'list', - }, - 'edit', - ); - - assert - .dom('[data-test-field-container] [data-test-multiple-image-field]') - .exists('Multiple image field edit view is rendered'); - - assert - .dom('[data-test-field-container] .images-container.variant-list') - .exists('List variant renders list container'); - }); - - test('gallery variant renders gallery upload component', async function (assert) { - await renderConfiguredField( - { - images: [{ imageUrl: 'https://example.com/image1.jpg' }], - }, - { - variant: 'gallery', - }, - 'edit', - ); - - assert - .dom('[data-test-field-container] [data-test-multiple-image-field]') - .exists('Multiple image field edit view is rendered'); - - assert - .dom('[data-test-field-container] .images-container.variant-gallery') - .exists('Gallery variant renders gallery container'); - }); - - test('dropzone variant renders dropzone upload component', async function (assert) { - await renderConfiguredField( - { - images: [{ imageUrl: 'https://example.com/image1.jpg' }], - }, - { - variant: 'dropzone', - }, - 'edit', - ); - - assert - .dom('[data-test-field-container] [data-test-multiple-image-field]') - .exists('Multiple image field edit view is rendered'); - - assert - .dom('[data-test-field-container] .images-container.variant-dropzone') - .exists('Dropzone variant renders dropzone container'); - }); - - test('invalid variant falls back to default list', async function (assert) { - await renderConfiguredField( - { images: [] }, - { - variant: 'invalid-variant', - }, - 'edit', - ); - - // Should fall back to list variant (default) - assert - .dom('[data-test-field-container] [data-test-multiple-image-field]') - .exists('Multiple image field with invalid variant still renders'); - }); - - // MultipleImageField Presentation Tests - test('grid presentation renders grid presentation component', async function (assert) { - await renderConfiguredField( - { - images: [ - { imageUrl: 'https://example.com/image1.jpg' }, - { imageUrl: 'https://example.com/image2.jpg' }, - ], - }, - { - variant: 'list', - presentation: 'grid', - }, - 'embedded', - ); - - assert - .dom('[data-test-field-container] .images-grid') - .exists('Grid presentation renders grid component'); - }); - - test('carousel presentation renders carousel presentation component', async function (assert) { - await renderConfiguredField( - { - images: [ - { imageUrl: 'https://example.com/image1.jpg' }, - { imageUrl: 'https://example.com/image2.jpg' }, - ], - }, - { - variant: 'list', - presentation: 'carousel', - }, - 'embedded', - ); - - assert - .dom('[data-test-field-container] .carousel-container') - .exists('Carousel presentation renders carousel component'); - }); - - test('invalid presentation falls back to default grid', async function (assert) { - await renderConfiguredField( - { - images: [{ imageUrl: 'https://example.com/image1.jpg' }], - }, - { - variant: 'list', - presentation: 'invalid-presentation', - }, - 'embedded', - ); - - // Should fall back to grid presentation (default) - assert - .dom( - '[data-test-field-container] [data-test-multiple-image-field], [data-test-field-container] .images-grid', - ) - .exists('Multiple image field with invalid presentation still renders'); - }); - - // MultipleImageField Options Tests - test('allowBatchSelect option controls batch actions visibility', async function (assert) { - // Test with allowBatchSelect: true - await renderConfiguredField( - { - images: [{ imageUrl: 'https://example.com/image1.jpg' }], - }, - { - variant: 'list', - options: { - allowBatchSelect: true, - }, - }, - 'edit', - ); - - assert - .dom('[data-test-field-container] .batch-actions') - .exists( - 'Multiple image field with allowBatchSelect: true shows batch actions', - ); - - // Test with allowBatchSelect: false - await renderConfiguredField( - { - images: [{ imageUrl: 'https://example.com/image1.jpg' }], - }, - { - variant: 'list', - options: { - allowBatchSelect: false, - }, - }, - 'edit', - ); - - assert - .dom('[data-test-field-container] .batch-actions') - .doesNotExist( - 'Multiple image field with allowBatchSelect: false hides batch actions', - ); - }); - - test('multiple image field ignores irrelevant config properties', async function (assert) { - await renderConfiguredField( - { images: [] }, - { - variant: 'list', - // These properties should be ignored by multiple image field - showImageModal: true, - type: 'rating', - maxStars: 5, - }, - 'edit', - ); - - // Multiple image field should still work normally, ignoring the irrelevant config - assert - .dom('[data-test-field-container] [data-test-multiple-image-field]') - .exists( - 'Multiple image field ignores showImageModal, type, maxStars configs', - ); - }); - - // Default Config Fallback Tests - test('multiple image field edit view falls back to default list variant when config is missing', async function (assert) { - await renderConfiguredField({ images: [] }, {}, 'edit'); - - assert - .dom('[data-test-field-container] [data-test-multiple-image-field]') - .exists( - 'Multiple image field edit view renders with default list variant', - ); - }); - - test('multiple image field embedded view falls back to default grid presentation when config is missing', async function (assert) { - await renderConfiguredField( - { - images: [{ imageUrl: 'https://example.com/image1.jpg' }], - }, - {}, - 'embedded', - ); - - assert - .dom('[data-test-field-container] .images-grid') - .exists( - 'Multiple image field embedded view defaults to grid presentation', - ); - }); -}); diff --git a/packages/host/tests/integration/number-field-configuration-test.gts b/packages/host/tests/integration/number-field-configuration-test.gts deleted file mode 100644 index 6023703fc87..00000000000 --- a/packages/host/tests/integration/number-field-configuration-test.gts +++ /dev/null @@ -1,293 +0,0 @@ -import { getService } from '@universal-ember/test-support'; -import { module, test } from 'qunit'; - -import { ensureTrailingSlash } from '@cardstack/runtime-common'; -import type { Loader } from '@cardstack/runtime-common/loader'; - -import ENV from '@cardstack/host/config/environment'; - -import { - setupBaseRealm, - field, - contains, - CardDef, - Component, -} from '../helpers/base-realm'; -import { renderCard } from '../helpers/render-component'; -import { setupRenderingTest } from '../helpers/setup'; - -type FieldFormat = 'embedded' | 'atom' | 'edit'; - -let loader: Loader; - -module('Integration | number field configuration', function (hooks) { - setupRenderingTest(hooks); - setupBaseRealm(hooks); - - let catalogRealmURL = ensureTrailingSlash(ENV.resolvedCatalogRealmURL); - let CatalogNumberFieldClass: any; - - hooks.beforeEach(async function () { - loader = getService('loader-service').loader; - const numberModule: any = await loader.import( - `${catalogRealmURL}fields/number`, - ); - CatalogNumberFieldClass = numberModule.default; - }); - - async function renderConfiguredField( - value: unknown, - configuration: any, - format: FieldFormat = 'atom', - ) { - const fieldFormat = format; - - class TestCard extends CardDef { - @field sample = contains(CatalogNumberFieldClass, { configuration }); - - static isolated = class Isolated extends Component { - format: FieldFormat = fieldFormat; - - - }; - } - - let card = new TestCard({ sample: value }); - await renderCard(loader, card, 'isolated'); - } - - // ============================================ - // Presentation Mode Rendering Tests - // ============================================ - - test('each presentation mode renders correct embedded component', async function (assert) { - const presentations = [ - { mode: 'standard', embeddedClass: '[data-test-number-field-embedded]' }, - { mode: 'stat', embeddedClass: '.stat-field-embedded' }, - { mode: 'score', embeddedClass: '.score-field-embedded' }, - { mode: 'progress-bar', embeddedClass: '.progress-bar-container' }, - { mode: 'progress-circle', embeddedClass: '.progress-circle-container' }, - { - mode: 'badge-notification', - embeddedClass: '.badge-notification-embedded', - }, - { mode: 'badge-metric', embeddedClass: '.badge-metric-embedded' }, - { mode: 'badge-counter', embeddedClass: '.badge-counter-embedded' }, - { mode: 'gauge', embeddedClass: '.gauge-embedded' }, - ]; - - for (const { mode, embeddedClass } of presentations) { - await renderConfiguredField(50, { presentation: mode }, 'embedded'); - assert - .dom(`[data-test-field-container] ${embeddedClass}`) - .exists(`${mode} presentation renders correct embedded component`); - } - }); - - // ============================================ - // Options Configuration Tests - // ============================================ - - test('prefix/suffix/decimals options work correctly', async function (assert) { - await renderConfiguredField( - 100.5, - { - presentation: 'standard', - options: { - prefix: '$', - suffix: ' USD', - decimals: 2, - }, - }, - 'atom', - ); - - assert - .dom('[data-test-field-container] [data-test-number-field-atom]') - .hasTextContaining('$100.50 USD', 'Prefix/suffix/decimals are applied'); - }); - - test('min/max options work in edit mode', async function (assert) { - await renderConfiguredField( - 200, - { - presentation: 'standard', - options: { - min: 0, - max: 100, - }, - }, - 'edit', - ); - - assert - .dom('[data-test-field-container] input[type="number"]') - .hasAttribute('min', '0', 'Edit mode respects min') - .hasAttribute('max', '100', 'Edit mode respects max'); - }); - - test('valueFormat percentage with suffix works correctly', async function (assert) { - await renderConfiguredField( - 75, - { - presentation: 'progress-bar', - options: { - min: 0, - max: 100, - valueFormat: 'percentage', - suffix: 'completed', - showValue: true, - }, - }, - 'embedded', - ); - - assert - .dom('[data-test-field-container]') - .hasTextContaining('75% completed', 'Percentage format appends suffix'); - }); - - test('badge-notification respects max option for overflow', async function (assert) { - await renderConfiguredField( - 150, - { - presentation: 'badge-notification', - options: { - max: 99, - }, - }, - 'atom', - ); - - assert - .dom('[data-test-field-container] .badge-count') - .hasText('99+', 'Shows overflow indicator when max exceeded'); - }); - - // ============================================ - // Error Handling and Fallback Tests - // ============================================ - - test('invalid presentation falls back to standard', async function (assert) { - await renderConfiguredField( - 42, - { - presentation: 'nonexistent-presentation', - options: { - prefix: '$', - }, - }, - 'atom', - ); - - assert - .dom('[data-test-field-container] [data-test-number-field-atom]') - .exists('Invalid presentation falls back to standard'); - - assert - .dom('[data-test-field-container] [data-test-number-field-atom]') - .hasTextContaining('$42', 'Fallback still respects options'); - }); - - test('missing presentation defaults to standard', async function (assert) { - await renderConfiguredField(42, {}, 'atom'); - - assert - .dom('[data-test-field-container] [data-test-number-field-atom]') - .exists('Missing presentation defaults to standard'); - }); - - test('wrong type in options is ignored gracefully', async function (assert) { - await renderConfiguredField( - 75, - { - presentation: 'stat', - options: { - min: 'invalid' as any, - max: 'invalid' as any, - decimals: 'not-a-number' as any, - }, - }, - 'embedded', - ); - - assert - .dom('[data-test-field-container] .stat-field-embedded') - .exists('Renders even with wrong option types'); - - assert - .dom('[data-test-field-container] .stat-footer') - .doesNotExist('Does not show range with invalid min/max'); - }); - - test('null/undefined value is handled gracefully', async function (assert) { - await renderConfiguredField( - null, - { - presentation: 'stat', - options: { - placeholder: 'No value', - }, - }, - 'embedded', - ); - - assert - .dom('[data-test-field-container] .stat-value') - .hasText('No value', 'Shows placeholder for null value'); - }); - - // ============================================ - // Edit Mode Tests - // ============================================ - - test('edit mode always uses NumberInput regardless of presentation', async function (assert) { - await renderConfiguredField( - 50, - { - presentation: 'stat', - options: { - prefix: '$', - suffix: ' USD', - }, - }, - 'edit', - ); - - assert - .dom('[data-test-field-container] [data-test-number-input]') - .exists('Edit mode uses NumberInput for all presentations'); - - assert - .dom('[data-test-field-container] input[type="number"]') - .exists('Edit mode renders number input'); - }); - - test('edit mode extracts compatible options from any presentation', async function (assert) { - await renderConfiguredField( - 75, - { - presentation: 'progress-bar', - options: { - min: 0, - max: 100, - prefix: '$', - suffix: ' USD', - decimals: 2, - useGradient: true, - showValue: true, - }, - }, - 'edit', - ); - - assert - .dom('[data-test-field-container] input[type="number"]') - .hasAttribute('min', '0', 'Extracts min from options') - .hasAttribute('max', '100', 'Extracts max from options'); - }); -}); diff --git a/packages/realm-server/package.json b/packages/realm-server/package.json index 7fe71b0a5fa..501aaddaf69 100644 --- a/packages/realm-server/package.json +++ b/packages/realm-server/package.json @@ -102,7 +102,7 @@ "start:base": "./scripts/start-base.sh --workerManagerPort=4213", "start:test-realms": "./scripts/start-test-realms.sh --workerManagerPort=4211", "start:all": "./scripts/start-all.sh", - "start:skip-experiments": "./scripts/start-all-except-experiments.sh", + "start:skip-optional-realms": "./scripts/start-all-except-optional.sh", "start:services-for-host-tests": "./scripts/start-services-for-host-tests.sh", "start:without-matrix": "./scripts/start-without-matrix.sh", "start:staging": "./scripts/start-staging.sh", diff --git a/packages/realm-server/scripts/start-all-except-experiments.sh b/packages/realm-server/scripts/start-all-except-optional.sh similarity index 77% rename from packages/realm-server/scripts/start-all-except-experiments.sh rename to packages/realm-server/scripts/start-all-except-optional.sh index a1a8df05672..85779e0f6df 100755 --- a/packages/realm-server/scripts/start-all-except-experiments.sh +++ b/packages/realm-server/scripts/start-all-except-optional.sh @@ -1,6 +1,6 @@ #! /bin/sh -WAIT_ON_TIMEOUT=900000 SKIP_EXPERIMENTS=true SKIP_BOXEL_HOMEPAGE=true NODE_NO_WARNINGS=1 start-server-and-test \ +WAIT_ON_TIMEOUT=900000 SKIP_EXPERIMENTS=true SKIP_CATALOG=true SKIP_BOXEL_HOMEPAGE=true NODE_NO_WARNINGS=1 start-server-and-test \ 'run-p start:pg start:prerender-dev start:prerender-manager-dev start:matrix start:smtp start:worker-development start:development' \ 'http-get://localhost:4201/base/_readiness-check?acceptHeader=application%2Fvnd.api%2Bjson|http://localhost:8008|http://localhost:5001' \ 'run-p start:worker-test start:test-realms' \ diff --git a/packages/realm-server/scripts/start-services-for-host-tests.sh b/packages/realm-server/scripts/start-services-for-host-tests.sh index b6dae25abd3..7fdeeb5ae3f 100755 --- a/packages/realm-server/scripts/start-services-for-host-tests.sh +++ b/packages/realm-server/scripts/start-services-for-host-tests.sh @@ -48,6 +48,7 @@ export CATALOG_REALM_PATH="$CATALOG_TEMP_PATH" WAIT_ON_TIMEOUT=900000 \ SKIP_EXPERIMENTS=true \ + SKIP_CATALOG=true \ SKIP_BOXEL_HOMEPAGE=true \ CATALOG_REALM_PATH="$CATALOG_TEMP_PATH" \ NODE_NO_WARNINGS=1 \ From d24fab188c5f49765df00658b694456060bcd7b2 Mon Sep 17 00:00:00 2001 From: Richard Tan Date: Thu, 5 Feb 2026 17:42:35 +0800 Subject: [PATCH 2/3] Host test helpers to skip catalog realm conditionally --- .../commands/set-user-system-card-test.gts | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 packages/host/tests/integration/commands/set-user-system-card-test.gts diff --git a/packages/host/tests/integration/commands/set-user-system-card-test.gts b/packages/host/tests/integration/commands/set-user-system-card-test.gts new file mode 100644 index 00000000000..efd865f7332 --- /dev/null +++ b/packages/host/tests/integration/commands/set-user-system-card-test.gts @@ -0,0 +1,63 @@ +import { getOwner } from '@ember/owner'; +import type { RenderingTestContext } from '@ember/test-helpers'; + +import { getService } from '@universal-ember/test-support'; +import { module, test } from 'qunit'; + +import SetUserSystemCardCommand from '@cardstack/host/commands/set-user-system-card'; +import RealmService from '@cardstack/host/services/realm'; + +import { + setupIntegrationTestRealm, + setupLocalIndexing, + testRealmInfo, + testRealmURL, +} from '../../helpers'; + +import { setupMockMatrix } from '../../helpers/mock-matrix'; +import { setupRenderingTest } from '../../helpers/setup'; + +class StubRealmService extends RealmService { + get defaultReadableRealm() { + return { + path: testRealmURL, + info: testRealmInfo, + }; + } +} + +module('Integration | commands | set-user-system-card', function (hooks) { + setupRenderingTest(hooks); + setupLocalIndexing(hooks); + + let mockMatrixUtils = setupMockMatrix(hooks, { + loggedInAs: '@testuser:localhost', + activeRealms: [testRealmURL], + }); + + hooks.beforeEach(function (this: RenderingTestContext) { + getOwner(this)!.register('service:realm', StubRealmService); + }); + + hooks.beforeEach(async function () { + await setupIntegrationTestRealm({ + mockMatrixUtils, + contents: {}, + }); + }); + + test('sets the system card account data', async function (assert) { + let commandService = getService('command-service'); + let command = new SetUserSystemCardCommand(commandService.commandContext); + + let systemCardId = 'http://localhost:4201/catalog/SystemCard/default'; + + await command.execute({ + cardId: systemCardId, + }); + + assert.deepEqual(mockMatrixUtils.getSystemCardAccountData(), { + id: systemCardId, + }); + }); +}); From 85917e1f93955049f0b64f3dfbbf282851fef7d5 Mon Sep 17 00:00:00 2001 From: Richard Tan Date: Mon, 9 Feb 2026 18:33:44 +0800 Subject: [PATCH 3/3] Fix test --- packages/host/app/lib/utils.ts | 6 +++--- packages/host/config/environment.js | 18 ++++++++++++------ .../tests/acceptance/ai-assistant-test.gts | 4 ++-- .../operator-mode-acceptance-test.gts | 8 ++++---- .../host/tests/helpers/mock-matrix/_client.ts | 5 +++-- .../tests/helpers/realm-server-mock/routes.ts | 2 +- .../commands/set-user-system-card-test.gts | 2 +- .../components/ai-module-creation-test.gts | 7 ++++--- .../components/card-catalog-test.gts | 10 +++++----- packages/host/tests/integration/realm-test.gts | 8 ++++---- 10 files changed, 39 insertions(+), 31 deletions(-) diff --git a/packages/host/app/lib/utils.ts b/packages/host/app/lib/utils.ts index b348bf74e1e..873e5c0ad73 100644 --- a/packages/host/app/lib/utils.ts +++ b/packages/host/app/lib/utils.ts @@ -149,9 +149,9 @@ export function urlForRealmLookup(card: CardDef) { } // usage example for realm url: `catalogRealm.url`, `skillsRealm.url` -export const catalogRealm = new RealmPaths( - new URL(ENV.resolvedCatalogRealmURL), -); +export const catalogRealm = ENV.resolvedCatalogRealmURL + ? new RealmPaths(new URL(ENV.resolvedCatalogRealmURL)) + : null; export const skillsRealm = new RealmPaths(new URL(ENV.resolvedSkillsRealmURL)); /** diff --git a/packages/host/config/environment.js b/packages/host/config/environment.js index 7772c10b202..7e19eff5cbb 100644 --- a/packages/host/config/environment.js +++ b/packages/host/config/environment.js @@ -55,9 +55,10 @@ module.exports = function (environment) { realmServerURL: process.env.REALM_SERVER_DOMAIN || 'http://localhost:4201/', resolvedBaseRealmURL: process.env.RESOLVED_BASE_REALM_URL || 'http://localhost:4201/base/', - resolvedCatalogRealmURL: - process.env.RESOLVED_CATALOG_REALM_URL || - 'http://localhost:4201/catalog/', + resolvedCatalogRealmURL: process.env.SKIP_CATALOG + ? undefined + : process.env.RESOLVED_CATALOG_REALM_URL || + 'http://localhost:4201/catalog/', resolvedSkillsRealmURL: process.env.RESOLVED_SKILLS_REALM_URL || 'http://localhost:4201/skills/', featureFlags: { @@ -71,9 +72,11 @@ module.exports = function (environment) { // ENV.APP.LOG_TRANSITIONS = true; // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; // ENV.APP.LOG_VIEW_LOOKUPS = true; - ENV.defaultSystemCardId = - process.env.DEFAULT_SYSTEM_CARD_ID ?? - 'http://localhost:4201/catalog/SystemCard/default'; + ENV.defaultSystemCardId = process.env.DEFAULT_SYSTEM_CARD_ID; + if (!ENV.defaultSystemCardId && !process.env.SKIP_CATALOG) { + ENV.defaultSystemCardId = + 'http://localhost:4201/catalog/SystemCard/default'; + } } if (environment === 'test') { @@ -98,6 +101,9 @@ module.exports = function (environment) { SHOW_ASK_AI: true, }; + // Catalog realm is not available in test environment + ENV.resolvedCatalogRealmURL = undefined; + ENV.defaultSystemCardId = process.env.DEFAULT_SYSTEM_CARD_ID ?? 'http://test-realm/test/SystemCard/default'; diff --git a/packages/host/tests/acceptance/ai-assistant-test.gts b/packages/host/tests/acceptance/ai-assistant-test.gts index 6eaa0dbe2b2..8e6c50e6ce6 100644 --- a/packages/host/tests/acceptance/ai-assistant-test.gts +++ b/packages/host/tests/acceptance/ai-assistant-test.gts @@ -2011,7 +2011,7 @@ module('Acceptance | AI Assistant tests', function (hooks) { type: 'user-workspace', url: testRealmURL, }, - { + catalogRealm && { name: 'Cardstack Catalog', type: 'catalog-workspace', url: catalogRealm.url, @@ -2021,7 +2021,7 @@ module('Acceptance | AI Assistant tests', function (hooks) { type: 'catalog-workspace', url: skillsRealm.url, }, - ], + ].filter(Boolean), 'Context sent with message contains correct workspaces', ); }); diff --git a/packages/host/tests/acceptance/operator-mode-acceptance-test.gts b/packages/host/tests/acceptance/operator-mode-acceptance-test.gts index 9ae492c84ad..c1c8150083b 100644 --- a/packages/host/tests/acceptance/operator-mode-acceptance-test.gts +++ b/packages/host/tests/acceptance/operator-mode-acceptance-test.gts @@ -61,7 +61,7 @@ import { } from '../helpers/recent-files-cards'; import { setupApplicationTest } from '../helpers/setup'; -const catalogRealmURL = ensureTrailingSlash(ENV.resolvedCatalogRealmURL); +const skillsRealmURL = ensureTrailingSlash(ENV.resolvedSkillsRealmURL); let matrixRoomId: string; let realm2URL = 'http://test-realm/user/test2/'; @@ -809,10 +809,10 @@ module('Acceptance | operator mode tests', function (hooks) { await click('[data-test-workspace-chooser-toggle]'); - await click('[data-test-workspace="Cardstack Catalog"]'); + await click('[data-test-workspace="Boxel Skills"]'); await click('[data-test-submode-switcher] button'); await click('[data-test-boxel-menu-item-text="Code"]'); - assert.dom(`[data-test-realm-name]`).includesText('In Cardstack Catalog'); + assert.dom(`[data-test-realm-name]`).includesText('In Boxel Skills'); assert.dom(`[data-test-file="index.json"]`).hasClass('selected'); assert.dom('[data-test-recent-file]').exists({ count: 4 }); assert @@ -839,7 +839,7 @@ module('Acceptance | operator mode tests', function (hooks) { .dom(`[data-test-recent-file="${testRealmURL}Pet/vangogh.json"]`) .exists(); assert - .dom(`[data-test-recent-file="${catalogRealmURL}index.json"]`) + .dom(`[data-test-recent-file="${skillsRealmURL}index.json"]`) .exists(); assert .dom(`[data-test-recent-file="${testRealmURL}Pet/mango.json"]`) diff --git a/packages/host/tests/helpers/mock-matrix/_client.ts b/packages/host/tests/helpers/mock-matrix/_client.ts index 108f959dbe0..d710ae12722 100644 --- a/packages/host/tests/helpers/mock-matrix/_client.ts +++ b/packages/host/tests/helpers/mock-matrix/_client.ts @@ -60,9 +60,10 @@ type Plural = { const publicRealmURLs = [ baseRealm.url, - ensureTrailingSlash(ENV.resolvedCatalogRealmURL), + ENV.resolvedCatalogRealmURL && + ensureTrailingSlash(ENV.resolvedCatalogRealmURL), ensureTrailingSlash(ENV.resolvedSkillsRealmURL), -]; +].filter(Boolean) as string[]; export class MockClient implements ExtendedClient { private listeners: Partial> = {}; diff --git a/packages/host/tests/helpers/realm-server-mock/routes.ts b/packages/host/tests/helpers/realm-server-mock/routes.ts index c258a39e843..90f79da070e 100644 --- a/packages/host/tests/helpers/realm-server-mock/routes.ts +++ b/packages/host/tests/helpers/realm-server-mock/routes.ts @@ -213,7 +213,7 @@ function registerCatalogRoutes() { ENV.resolvedSkillsRealmURL, ] .filter(Boolean) - .map((url) => ensureTrailingSlash(url)); + .map((url) => ensureTrailingSlash(url as string)); let data = catalogURLs.map((realmURL) => ({ id: realmURL, type: 'catalog-realm', diff --git a/packages/host/tests/integration/commands/set-user-system-card-test.gts b/packages/host/tests/integration/commands/set-user-system-card-test.gts index efd865f7332..850deb942fe 100644 --- a/packages/host/tests/integration/commands/set-user-system-card-test.gts +++ b/packages/host/tests/integration/commands/set-user-system-card-test.gts @@ -50,7 +50,7 @@ module('Integration | commands | set-user-system-card', function (hooks) { let commandService = getService('command-service'); let command = new SetUserSystemCardCommand(commandService.commandContext); - let systemCardId = 'http://localhost:4201/catalog/SystemCard/default'; + let systemCardId = `${testRealmURL}SystemCard/default`; await command.execute({ cardId: systemCardId, diff --git a/packages/host/tests/integration/components/ai-module-creation-test.gts b/packages/host/tests/integration/components/ai-module-creation-test.gts index e1ff04242e0..1caf64d0d86 100644 --- a/packages/host/tests/integration/components/ai-module-creation-test.gts +++ b/packages/host/tests/integration/components/ai-module-creation-test.gts @@ -45,7 +45,7 @@ module('Integration | create app module via ai-assistant', function (hooks) { let loader: Loader; let operatorModeStateService: OperatorModeStateService; - const catalogRealmURL = ensureTrailingSlash(ENV.resolvedCatalogRealmURL); + const skillsRealmURL = ensureTrailingSlash(ENV.resolvedSkillsRealmURL); setupRenderingTest(hooks); setupOperatorModeStateCleanup(hooks); @@ -147,7 +147,7 @@ module('Integration | create app module via ai-assistant', function (hooks) { }, meta: { adoptsFrom: { - module: `${catalogRealmURL}product-requirement-document`, + module: `${skillsRealmURL}product-requirement-document`, name: 'ProductRequirementDocument', }, }, @@ -200,7 +200,8 @@ module('Integration | create app module via ai-assistant', function (hooks) { 'skill card is attached', ); - const moduleCode = `import { Component, CardDef, FieldDef, linksTo, linksToMany, field, contains, containsMany } from 'https://cardstack.com/base/card-api';\nimport StringField from 'https://cardstack.com/base/string';\nimport BooleanField from 'https://cardstack.com/base/boolean';\nimport DateField from 'https://cardstack.com/base/date';\nimport DateTimeField from 'https://cardstack.com/base/datetime';\nimport NumberField from 'https://cardstack.com/base/number';\nimport MarkdownField from 'https://cardstack.com/base/markdown';\nimport { AppCard } from '${catalogRealmURL}app-card';\n\nexport class Tour extends CardDef {\n static displayName = 'Tour';\n\n @field tourID = contains(StringField);\n @field date = contains(DateField);\n @field time = contains(DateTimeField);\n @field parentNames = contains(StringField);\n @field contactInformation = contains(StringField);\n @field notes = contains(MarkdownField);\n\n @field parents = linksToMany(() => Parent);\n}\n\nexport class Student extends CardDef {\n static displayName = 'Student';\n\n @field studentID = contains(StringField);\n @field name = contains(StringField);\n @field age = contains(NumberField);\n @field enrollmentDate = contains(DateField);\n @field parentInformation = contains(MarkdownField);\n @field allergiesMedicalNotes = contains(MarkdownField);\n @field attendanceRecords = containsMany(MarkdownField);\n \n @field parents = linksToMany(() => Parent);\n @field classes = linksToMany(() => Class);\n}\n\nexport class Parent extends CardDef {\n static displayName = 'Parent';\n\n @field parentID = contains(StringField);\n @field name = contains(StringField);\n @field contactInformation = contains(StringField);\n \n @field students = linksToMany(Student);\n @field tours = linksToMany(Tour);\n}\n\nexport class Staff extends CardDef {\n static displayName = 'Staff';\n\n @field staffID = contains(StringField);\n @field name = contains(StringField);\n @field role = contains(StringField);\n @field contactInformation = contains(StringField);\n @field schedule = contains(MarkdownField);\n}\n\nexport class Class extends CardDef {\n static displayName = 'Class';\n\n @field classID = contains(StringField);\n @field name = contains(StringField);\n @field schedule = contains(MarkdownField);\n \n @field instructor = linksTo(Staff);\n @field enrolledStudents = linksToMany(Student);\n}\n\nexport class Communication extends CardDef {\n static displayName = 'Communication';\n\n @field communicationID = contains(StringField);\n @field date = contains(DateField);\n @field type = contains(StringField);\n @field content = contains(MarkdownField);\n @field followUpDate = contains(DateField);\n}\n\nexport class PreschoolCRMApp extends AppCard {\n static displayName = 'Preschool CRM';\n\n @field tours = containsMany(Tour);\n @field students = containsMany(Student);\n @field parents = containsMany(Parent);\n @field staff = containsMany(Staff);\n @field classes = containsMany(Class);\n @field communications = containsMany(Communication);\n}\n`; + // Remove catalog app card dependency for now since we don't have catalog in tests + const moduleCode = `import { Component, CardDef, FieldDef, linksTo, linksToMany, field, contains, containsMany } from 'https://cardstack.com/base/card-api';\nimport StringField from 'https://cardstack.com/base/string';\nimport BooleanField from 'https://cardstack.com/base/boolean';\nimport DateField from 'https://cardstack.com/base/date';\nimport DateTimeField from 'https://cardstack.com/base/datetime';\nimport NumberField from 'https://cardstack.com/base/number';\nimport MarkdownField from 'https://cardstack.com/base/markdown';\n\nexport class Tour extends CardDef {\n static displayName = 'Tour';\n\n @field tourID = contains(StringField);\n @field date = contains(DateField);\n @field time = contains(DateTimeField);\n @field parentNames = contains(StringField);\n @field contactInformation = contains(StringField);\n @field notes = contains(MarkdownField);\n\n @field parents = linksToMany(() => Parent);\n}\n\nexport class Student extends CardDef {\n static displayName = 'Student';\n\n @field studentID = contains(StringField);\n @field name = contains(StringField);\n @field age = contains(NumberField);\n @field enrollmentDate = contains(DateField);\n @field parentInformation = contains(MarkdownField);\n @field allergiesMedicalNotes = contains(MarkdownField);\n @field attendanceRecords = containsMany(MarkdownField);\n \n @field parents = linksToMany(() => Parent);\n @field classes = linksToMany(() => Class);\n}\n\nexport class Parent extends CardDef {\n static displayName = 'Parent';\n\n @field parentID = contains(StringField);\n @field name = contains(StringField);\n @field contactInformation = contains(StringField);\n \n @field students = linksToMany(Student);\n @field tours = linksToMany(Tour);\n}\n\nexport class Staff extends CardDef {\n static displayName = 'Staff';\n\n @field staffID = contains(StringField);\n @field name = contains(StringField);\n @field role = contains(StringField);\n @field contactInformation = contains(StringField);\n @field schedule = contains(MarkdownField);\n}\n\nexport class Class extends CardDef {\n static displayName = 'Class';\n\n @field classID = contains(StringField);\n @field name = contains(StringField);\n @field schedule = contains(MarkdownField);\n \n @field instructor = linksTo(Staff);\n @field enrolledStudents = linksToMany(Student);\n}\n\nexport class Communication extends CardDef {\n static displayName = 'Communication';\n\n @field communicationID = contains(StringField);\n @field date = contains(DateField);\n @field type = contains(StringField);\n @field content = contains(MarkdownField);\n @field followUpDate = contains(DateField);\n}\n\nexport class PreschoolCRMApp extends CardDef {\n static displayName = 'Preschool CRM';\n\n @field tours = containsMany(Tour);\n @field students = containsMany(Student);\n @field parents = containsMany(Parent);\n @field staff = containsMany(Staff);\n @field classes = containsMany(Class);\n @field communications = containsMany(Communication);\n}\n`; simulateRemoteMessage(roomId, '@aibot:localhost', { msgtype: APP_BOXEL_MESSAGE_MSGTYPE, diff --git a/packages/host/tests/integration/components/card-catalog-test.gts b/packages/host/tests/integration/components/card-catalog-test.gts index 0e5ea8a08e5..98447345398 100644 --- a/packages/host/tests/integration/components/card-catalog-test.gts +++ b/packages/host/tests/integration/components/card-catalog-test.gts @@ -208,7 +208,7 @@ module('Integration | card-catalog', function (hooks) { await waitFor(`[data-test-realm="${realmName}"]`); await waitFor('[data-test-realm="Base Workspace"]'); - assert.dom('[data-test-realm]').exists({ count: 3 }); + assert.dom('[data-test-realm]').exists({ count: 2 }); assert .dom(`[data-test-realm="${realmName}"] [data-test-results-count]`) .hasText('6 results'); @@ -238,8 +238,8 @@ module('Integration | card-catalog', function (hooks) { test('can filter cards by selecting a realm', async function (assert) { await click('[data-test-realm-filter-button]'); - assert.dom('[data-test-boxel-menu-item]').exists({ count: 4 }); - assert.dom('[data-test-boxel-menu-item-selected]').exists({ count: 4 }); // All realms are selected by default + assert.dom('[data-test-boxel-menu-item]').exists({ count: 3 }); + assert.dom('[data-test-boxel-menu-item-selected]').exists({ count: 3 }); // All realms are selected by default assert .dom('[data-test-realm-filter-button]') .includesText('Workspace: All'); @@ -247,7 +247,7 @@ module('Integration | card-catalog', function (hooks) { await click(`[data-test-boxel-menu-item-text="Local Workspace"]`); // Unselect Local Workspace assert .dom('[data-test-realm-filter-button]') - .hasText(`Workspace: Base Workspace, Cardstack Catalog, Boxel Skills`); + .hasText(`Workspace: Base Workspace, Boxel Skills`); assert .dom(`[data-test-realm="Base Workspace"] [data-test-card-catalog-item]`) .exists(); @@ -255,7 +255,7 @@ module('Integration | card-catalog', function (hooks) { assert.dom(`[data-test-realm="${realmName}"]`).doesNotExist(); await click('[data-test-realm-filter-button]'); - assert.dom('[data-test-boxel-menu-item-selected]').exists({ count: 3 }); + assert.dom('[data-test-boxel-menu-item-selected]').exists({ count: 2 }); assert .dom('[data-test-boxel-menu-item-selected]') .hasText('Base Workspace'); diff --git a/packages/host/tests/integration/realm-test.gts b/packages/host/tests/integration/realm-test.gts index e711987221a..d59fe452868 100644 --- a/packages/host/tests/integration/realm-test.gts +++ b/packages/host/tests/integration/realm-test.gts @@ -3312,7 +3312,7 @@ module('Integration | realm', function (hooks) { }); test('included card uses correct module path when realm is mounted', async function (assert) { - let catalogRealmURL = 'http://localhost:4201/catalog/'; + let mountedRealmURL = 'http://localhost:4201/mounted-test/'; let spreadsheet1Id = 'spreadsheet-1'; let spreadsheet2Id = 'spreadsheet-2'; @@ -3328,7 +3328,7 @@ module('Integration | realm', function (hooks) { let { realm } = await setupIntegrationTestRealm({ mockMatrixUtils, - realmURL: catalogRealmURL, + realmURL: mountedRealmURL, contents: { 'spreadsheet/spreadsheet.gts': { Spreadsheet, @@ -3389,7 +3389,7 @@ module('Integration | realm', function (hooks) { let response = await handle( realm, - new Request(`${catalogRealmURL}index`, { + new Request(`${mountedRealmURL}index`, { headers: { Accept: 'application/vnd.card+json', }, @@ -3400,7 +3400,7 @@ module('Integration | realm', function (hooks) { let included = json.included?.find( (resource: any) => resource.id === - `${catalogRealmURL}spreadsheet/Spreadsheet/${spreadsheet1Id}`, + `${mountedRealmURL}spreadsheet/Spreadsheet/${spreadsheet1Id}`, ); assert.ok(included, 'linked spreadsheet card is included'); assert.strictEqual(