Skip to content
Closed
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions e2e-tests/add_prompt_deep_link.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ test("add prompt via deep link with base64-encoded data", async ({
await po.navigation.goToLibraryTab();

// Verify library is empty initially
await expect(po.page.getByTestId("prompt-card")).not.toBeVisible();
await expect(po.page.getByTestId("library-prompt-card")).not.toBeVisible();

// Create the prompt data to be encoded
const promptData = {
Expand Down Expand Up @@ -48,5 +48,7 @@ test("add prompt via deep link with base64-encoded data", async ({
// Save the prompt
await po.page.getByRole("button", { name: "Save" }).click();

await expect(po.page.getByTestId("prompt-card")).toMatchAriaSnapshot();
await expect(
po.page.getByTestId("library-prompt-card"),
).toMatchAriaSnapshot();
});
139 changes: 139 additions & 0 deletions e2e-tests/media_library.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import fs from "fs";
import path from "path";
import { expect } from "@playwright/test";
import { testSkipIfWindows } from "./helpers/test_helper";
import type { PageObject } from "./helpers/test_helper";

const IMAGE_FIXTURE_PATH = path.join(
__dirname,
"fixtures",
"images",
"logo.png",
);

async function importAppAndSeedMedia({
po,
fixtureName,
files,
}: {
po: PageObject;
fixtureName: string;
files: string[];
}) {
await po.navigation.goToAppsTab();
await po.appManagement.importApp(fixtureName);

// Wait for the title bar to show the imported app name.
// getCurrentAppName() only checks "not 'no app selected'", which races
// on subsequent imports where the title bar already shows a previous app.
await expect(po.appManagement.getTitleBarAppNameButton()).toContainText(
fixtureName,
{ timeout: 15000 },
);

const appName = await po.appManagement.getCurrentAppName();
if (!appName) {
throw new Error("Failed to get app name after import");
}
const appPath = await po.appManagement.getCurrentAppPath();
const mediaDirPath = path.join(appPath, ".dyad", "media");
fs.mkdirSync(mediaDirPath, { recursive: true });

for (const fileName of files) {
fs.copyFileSync(IMAGE_FIXTURE_PATH, path.join(mediaDirPath, fileName));
}

return { appName, appPath, mediaDirPath };
}

async function openMediaFolderByAppName(po: PageObject, appName: string) {
const collapsedFolder = po.page
.locator('[data-testid^="media-folder-"]')
.filter({ hasText: appName })
.first();

await expect(collapsedFolder).toBeVisible({ timeout: 15000 });
await collapsedFolder.click();
await expect(po.page.getByTestId("media-folder-back-button")).toBeVisible();
}

async function openMediaActionsForFile(po: PageObject, fileName: string) {
const thumbnail = po.page
.getByTestId("media-thumbnail")
.filter({ hasText: fileName })
.first();

await expect(thumbnail).toBeVisible();
await thumbnail.getByTestId("media-file-actions-trigger").click();
}

testSkipIfWindows(
"media library - rename, move, delete, and start a new chat with image reference",
async ({ po }) => {
await po.setUp();

const sourceApp = await importAppAndSeedMedia({
po,
fixtureName: "minimal",
files: ["chat-image.png", "move-image.png"],
});
const targetApp = await importAppAndSeedMedia({
po,
fixtureName: "astro",
files: [],
});

await po.navigation.goToLibraryTab();
await po.page.getByRole("link", { name: "Media" }).click();

await openMediaFolderByAppName(po, sourceApp.appName);

await openMediaActionsForFile(po, "move-image.png");
await po.page.getByTestId("media-rename-image").click();
await po.page.getByTestId("media-rename-input").fill("renamed-image");
await po.page.getByTestId("media-rename-confirm-button").click();

const sourceRenamedPath = path.join(
sourceApp.mediaDirPath,
"renamed-image.png",
);
const sourceOldPath = path.join(sourceApp.mediaDirPath, "move-image.png");

await expect.poll(() => fs.existsSync(sourceRenamedPath)).toBe(true);
await expect.poll(() => fs.existsSync(sourceOldPath)).toBe(false);

await openMediaActionsForFile(po, "renamed-image.png");
await po.page.getByTestId("media-move-to-submenu").click();
await po.page.getByRole("menuitem", { name: targetApp.appName }).click();

const targetMovedPath = path.join(
targetApp.mediaDirPath,
"renamed-image.png",
);

await expect.poll(() => fs.existsSync(sourceRenamedPath)).toBe(false);
await expect.poll(() => fs.existsSync(targetMovedPath)).toBe(true);

await po.page.getByTestId("media-folder-back-button").click();
await openMediaFolderByAppName(po, targetApp.appName);

await openMediaActionsForFile(po, "renamed-image.png");
await po.page.getByTestId("media-delete-image").click();
await po.page.getByTestId("media-delete-confirm-button").click();

await expect.poll(() => fs.existsSync(targetMovedPath)).toBe(false);

// After deleting the last file from the target folder, the folder
// disappears from the listing and the view returns to the folder list.
await openMediaFolderByAppName(po, sourceApp.appName);

await openMediaActionsForFile(po, "chat-image.png");
await po.page.getByTestId("media-start-chat-with-image").click();

await expect(po.chatActions.getChatInput()).toBeVisible();
await expect(po.chatActions.getChatInput()).toContainText(
`@chat-image.png`,
);
expect(await po.appManagement.getCurrentAppName()).toBe(sourceApp.appName);
},
);
4 changes: 2 additions & 2 deletions e2e-tests/prompt_library.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ test("create and edit prompt", async ({ po }) => {
});

// Wait for prompt card to be fully rendered
const promptCard = po.page.getByTestId("prompt-card");
const promptCard = po.page.getByTestId("library-prompt-card");
await expect(promptCard).toBeVisible();
await expect(
promptCard.getByRole("heading", { name: "title1" }),
Expand Down Expand Up @@ -48,7 +48,7 @@ test("delete prompt", async ({ po }) => {
await po.page.getByTestId("delete-prompt-button").click();
await po.page.getByRole("button", { name: "Delete" }).click();

await expect(po.page.getByTestId("prompt-card")).not.toBeVisible();
await expect(po.page.getByTestId("library-prompt-card")).not.toBeVisible();
});

test("use prompt", async ({ po }) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
- img
- text: Prompt
- img
- heading "Deep Link Test Prompt" [level=3]
- paragraph: A prompt created via deep link
- button:
- text: "You are a helpful assistant. Please help with: [task here]"
- button "Edit prompt":
- img
- button:
- img
- text: "You are a helpful assistant. Please help with: [task here]"
- button "Delete prompt":
- img
6 changes: 3 additions & 3 deletions e2e-tests/themes_management.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ test("themes management - CRUD operations", async ({ po }) => {

// Verify dialog closes and theme card appears
await expect(po.page.getByRole("dialog")).not.toBeVisible();
await expect(po.page.getByTestId("theme-card")).toBeVisible();
await expect(po.page.getByTestId("library-theme-card")).toBeVisible();
await expect(po.page.getByText("My Test Theme")).toBeVisible();
await expect(po.page.getByText("A test theme description")).toBeVisible();

Expand Down Expand Up @@ -268,7 +268,7 @@ test("themes management - AI generator flow", async ({ po }) => {

// Verify dialog closes and theme card appears
await expect(po.page.getByRole("dialog")).not.toBeVisible();
await expect(po.page.getByTestId("theme-card")).toBeVisible();
await expect(po.page.getByTestId("library-theme-card")).toBeVisible();
await expect(po.page.getByText("AI Generated Theme")).toBeVisible();
await expect(po.page.getByText("Created via AI generator")).toBeVisible();
});
Expand Down Expand Up @@ -330,7 +330,7 @@ test("themes management - AI generator from website URL", async ({ po }) => {

// Verify dialog closes and theme card appears
await expect(po.page.getByRole("dialog")).not.toBeVisible();
const themeCard = po.page.getByTestId("theme-card");
const themeCard = po.page.getByTestId("library-theme-card");
await expect(themeCard).toBeVisible();
await expect(themeCard.getByText("Website Theme")).toBeVisible();
await expect(themeCard.getByText("Generated from website")).toBeVisible();
Expand Down
14 changes: 7 additions & 7 deletions package-lock.json

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

7 changes: 6 additions & 1 deletion src/__tests__/git_branch_handlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,12 @@ describe("handleDeleteBranch", () => {
});

it("throws generic error when branch only exists on remote for non-GitHub app", async () => {
const nonGithubApp = { id: 1, path: "test-app", githubOrg: null, githubRepo: null };
const nonGithubApp = {
id: 1,
path: "test-app",
githubOrg: null,
githubRepo: null,
};
vi.mocked(db.query.apps.findFirst).mockResolvedValue(nonGithubApp as any);
vi.mocked(gitListBranches).mockResolvedValue(["main"]);
vi.mocked(gitListRemoteBranches).mockResolvedValue(["main", "feature"]);
Expand Down
45 changes: 45 additions & 0 deletions src/__tests__/parse_media_mentions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { describe, expect, it } from "vitest";
import {
parseMediaMentions,
stripResolvedMediaMentions,
} from "../shared/parse_media_mentions";

describe("parseMediaMentions", () => {
it("parses @media mentions from prompt text", () => {
const prompt = "Check @media:cat.png and @media:dog.png please";

expect(parseMediaMentions(prompt)).toEqual(["cat.png", "dog.png"]);
});

it("parses @media mentions with URL-encoded filenames (e.g. spaces)", () => {
const prompt = "Check @media:my%20photo.png please";

expect(parseMediaMentions(prompt)).toEqual(["my%20photo.png"]);
});
});

describe("stripResolvedMediaMentions", () => {
it("keeps user text when media mention is followed by adjacent text", () => {
const prompt = "@media:cat.pngdescribe this image";

expect(stripResolvedMediaMentions(prompt, ["cat.png"])).toBe(
"describe this image",
);
});

it("strips only resolved mentions and preserves unresolved ones", () => {
const prompt = "Use @media:cat.png and @media:missing.png now";

expect(stripResolvedMediaMentions(prompt, ["cat.png"])).toBe(
"Use and @media:missing.png now",
);
});

it("strips URL-encoded mentions (filenames with spaces)", () => {
const prompt = "Check @media:my%20photo.png please";

expect(stripResolvedMediaMentions(prompt, ["my%20photo.png"])).toBe(
"Check please",
);
});
});
Loading
Loading