make it easy to write multiple e2e tests (#280)
This commit is contained in:
1
e2e-tests/fixtures/basic.md
Normal file
1
e2e-tests/fixtures/basic.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
This is a simple basic response
|
||||||
@@ -1,9 +1,86 @@
|
|||||||
import { test as base } from "@playwright/test";
|
import { test as base, Page, expect } from "@playwright/test";
|
||||||
import { findLatestBuild, parseElectronApp } from "electron-playwright-helpers";
|
import { findLatestBuild, parseElectronApp } from "electron-playwright-helpers";
|
||||||
import { ElectronApplication, _electron as electron } from "playwright";
|
import { ElectronApplication, _electron as electron } from "playwright";
|
||||||
|
|
||||||
const showDebugLogs = process.env.DEBUG_LOGS === "true";
|
const showDebugLogs = process.env.DEBUG_LOGS === "true";
|
||||||
|
|
||||||
|
class PageObject {
|
||||||
|
constructor(private page: Page) {}
|
||||||
|
|
||||||
|
async setUp() {
|
||||||
|
await this.goToSettingsTab();
|
||||||
|
await this.setUpTestProvider();
|
||||||
|
await this.setUpTestModel();
|
||||||
|
await this.goToAppsTab();
|
||||||
|
await this.selectTestModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
async dumpMessages() {
|
||||||
|
await expect(this.page.getByTestId("messages-list")).toMatchAriaSnapshot();
|
||||||
|
}
|
||||||
|
|
||||||
|
async waitForChatCompletion() {
|
||||||
|
await expect(
|
||||||
|
this.page.getByRole("button", { name: "Retry" }),
|
||||||
|
).toBeVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendPrompt(prompt: string) {
|
||||||
|
await this.page
|
||||||
|
.getByRole("textbox", { name: "Ask Dyad to build..." })
|
||||||
|
.click();
|
||||||
|
await this.page
|
||||||
|
.getByRole("textbox", { name: "Ask Dyad to build..." })
|
||||||
|
.fill(prompt);
|
||||||
|
await this.page.getByRole("button", { name: "Start new chat" }).click();
|
||||||
|
await this.waitForChatCompletion();
|
||||||
|
}
|
||||||
|
|
||||||
|
async selectTestModel() {
|
||||||
|
await this.page.getByRole("button", { name: "Model: Auto" }).click();
|
||||||
|
await this.page.getByText("test-provider").click();
|
||||||
|
await this.page.getByText("test-model").click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async setUpTestProvider() {
|
||||||
|
await this.page.getByText("Add custom providerConnect to").click();
|
||||||
|
// Fill out provider dialog
|
||||||
|
await this.page
|
||||||
|
.getByRole("textbox", { name: "Provider ID" })
|
||||||
|
.fill("testing");
|
||||||
|
await this.page.getByRole("textbox", { name: "Display Name" }).click();
|
||||||
|
await this.page
|
||||||
|
.getByRole("textbox", { name: "Display Name" })
|
||||||
|
.fill("test-provider");
|
||||||
|
await this.page.getByText("API Base URLThe base URL for").click();
|
||||||
|
await this.page
|
||||||
|
.getByRole("textbox", { name: "API Base URL" })
|
||||||
|
.fill("http://localhost:3500/v1");
|
||||||
|
await this.page.getByRole("button", { name: "Add Provider" }).click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async setUpTestModel() {
|
||||||
|
await this.page
|
||||||
|
.getByRole("heading", { name: "test-provider Needs Setup" })
|
||||||
|
.click();
|
||||||
|
await this.page.getByRole("button", { name: "Add Custom Model" }).click();
|
||||||
|
await this.page
|
||||||
|
.getByRole("textbox", { name: "Model ID*" })
|
||||||
|
.fill("test-model");
|
||||||
|
await this.page.getByRole("textbox", { name: "Model ID*" }).press("Tab");
|
||||||
|
await this.page.getByRole("textbox", { name: "Name*" }).fill("test-model");
|
||||||
|
await this.page.getByRole("button", { name: "Add Model" }).click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async goToSettingsTab() {
|
||||||
|
await this.page.getByRole("link", { name: "Settings" }).click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async goToAppsTab() {
|
||||||
|
await this.page.getByRole("link", { name: "Apps" }).click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// From https://github.com/microsoft/playwright/issues/8208#issuecomment-1435475930
|
// From https://github.com/microsoft/playwright/issues/8208#issuecomment-1435475930
|
||||||
//
|
//
|
||||||
// Note how we mark the fixture as { auto: true }.
|
// Note how we mark the fixture as { auto: true }.
|
||||||
@@ -11,7 +88,17 @@ const showDebugLogs = process.env.DEBUG_LOGS === "true";
|
|||||||
export const test = base.extend<{
|
export const test = base.extend<{
|
||||||
attachScreenshotsToReport: void;
|
attachScreenshotsToReport: void;
|
||||||
electronApp: ElectronApplication;
|
electronApp: ElectronApplication;
|
||||||
|
po: PageObject;
|
||||||
}>({
|
}>({
|
||||||
|
po: [
|
||||||
|
async ({ electronApp }, use) => {
|
||||||
|
const page = await electronApp.firstWindow();
|
||||||
|
|
||||||
|
const po = new PageObject(page);
|
||||||
|
await use(po);
|
||||||
|
},
|
||||||
|
{ auto: true },
|
||||||
|
],
|
||||||
attachScreenshotsToReport: [
|
attachScreenshotsToReport: [
|
||||||
async ({ page }, use, testInfo) => {
|
async ({ page }, use, testInfo) => {
|
||||||
await use();
|
await use();
|
||||||
|
|||||||
@@ -8,45 +8,14 @@ test("renders the first page", async ({ electronApp }) => {
|
|||||||
expect(text).toBe("Build your dream app");
|
expect(text).toBe("Build your dream app");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("simple message to custom test model", async ({ electronApp }) => {
|
test("simple message to custom test model", async ({ po }) => {
|
||||||
const page = await electronApp.firstWindow();
|
await po.setUp();
|
||||||
await page.getByRole("link", { name: "Settings" }).click();
|
await po.sendPrompt("hi");
|
||||||
await page.getByText("Add custom providerConnect to").click();
|
await po.dumpMessages();
|
||||||
|
});
|
||||||
// Fill out provider dialog
|
|
||||||
await page.getByRole("textbox", { name: "Provider ID" }).fill("testing");
|
test("basic message to custom test model", async ({ po }) => {
|
||||||
await page.getByRole("textbox", { name: "Display Name" }).click();
|
await po.setUp();
|
||||||
await page
|
await po.sendPrompt("tc=basic");
|
||||||
.getByRole("textbox", { name: "Display Name" })
|
await po.dumpMessages();
|
||||||
.fill("test-provider");
|
|
||||||
await page.getByText("API Base URLThe base URL for").click();
|
|
||||||
await page
|
|
||||||
.getByRole("textbox", { name: "API Base URL" })
|
|
||||||
.fill("http://localhost:3500/v1");
|
|
||||||
await page.getByRole("button", { name: "Add Provider" }).click();
|
|
||||||
|
|
||||||
// Create custom model
|
|
||||||
await page
|
|
||||||
.getByRole("heading", { name: "test-provider Needs Setup" })
|
|
||||||
.click();
|
|
||||||
await page.getByRole("button", { name: "Add Custom Model" }).click();
|
|
||||||
await page.getByRole("textbox", { name: "Model ID*" }).fill("test-model");
|
|
||||||
await page.getByRole("textbox", { name: "Model ID*" }).press("Tab");
|
|
||||||
await page.getByRole("textbox", { name: "Name*" }).fill("test-model");
|
|
||||||
await page.getByRole("button", { name: "Add Model" }).click();
|
|
||||||
|
|
||||||
// Go to apps page and select custom model
|
|
||||||
await page.getByRole("link", { name: "Apps" }).click();
|
|
||||||
await page.getByRole("button", { name: "Model: Auto" }).click();
|
|
||||||
await page.getByText("test-provider").click();
|
|
||||||
await page.getByText("test-model").click();
|
|
||||||
|
|
||||||
// Enter prompt and send
|
|
||||||
await page.getByRole("textbox", { name: "Ask Dyad to build..." }).click();
|
|
||||||
await page.getByRole("textbox", { name: "Ask Dyad to build..." }).fill("hi");
|
|
||||||
await page.getByRole("button", { name: "Start new chat" }).click();
|
|
||||||
|
|
||||||
// Make sure it's done
|
|
||||||
await expect(page.getByRole("button", { name: "Retry" })).not.toBeVisible();
|
|
||||||
await expect(page.getByTestId("messages-list")).toMatchAriaSnapshot();
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
- paragraph: tc=basic
|
||||||
|
- paragraph: This is a simple basic response
|
||||||
|
- button "Retry":
|
||||||
|
- img
|
||||||
@@ -3,7 +3,7 @@ import { PlaywrightTestConfig } from "@playwright/test";
|
|||||||
const config: PlaywrightTestConfig = {
|
const config: PlaywrightTestConfig = {
|
||||||
testDir: "./e2e-tests",
|
testDir: "./e2e-tests",
|
||||||
maxFailures: 1,
|
maxFailures: 1,
|
||||||
timeout: process.env.CI ? 30_000 : 10_000,
|
timeout: process.env.CI ? 30_000 : 15_000,
|
||||||
// Use a custom snapshot path template because Playwright's default
|
// Use a custom snapshot path template because Playwright's default
|
||||||
// is platform-specific which isn't necessary for Dyad e2e tests
|
// is platform-specific which isn't necessary for Dyad e2e tests
|
||||||
// which should be platform agnostic (we don't do screenshots; only textual diffs).
|
// which should be platform agnostic (we don't do screenshots; only textual diffs).
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ export const TitleBar = () => {
|
|||||||
<div className="pl-20"></div>
|
<div className="pl-20"></div>
|
||||||
<img src={logo} alt="Dyad Logo" className="w-6 h-6 mr-2" />
|
<img src={logo} alt="Dyad Logo" className="w-6 h-6 mr-2" />
|
||||||
<Button
|
<Button
|
||||||
|
data-testid="title-bar-app-name-button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className={`hidden @md:block no-app-region-drag text-sm font-medium ${
|
className={`hidden @md:block no-app-region-drag text-sm font-medium ${
|
||||||
@@ -83,6 +84,7 @@ export const TitleBar = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
{isDyadPro && (
|
{isDyadPro && (
|
||||||
<Button
|
<Button
|
||||||
|
data-testid="title-bar-dyad-pro-button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate({
|
navigate({
|
||||||
to: providerSettingsRoute.id,
|
to: providerSettingsRoute.id,
|
||||||
|
|||||||
@@ -122,6 +122,11 @@ export const UserSettingsSchema = z.object({
|
|||||||
enableProSmartFilesContextMode: z.boolean().optional(),
|
enableProSmartFilesContextMode: z.boolean().optional(),
|
||||||
selectedTemplateId: z.string().optional(),
|
selectedTemplateId: z.string().optional(),
|
||||||
|
|
||||||
|
////////////////////////////////
|
||||||
|
// E2E TESTING ONLY.
|
||||||
|
////////////////////////////////
|
||||||
|
isTestMode: z.boolean().optional(),
|
||||||
|
|
||||||
////////////////////////////////
|
////////////////////////////////
|
||||||
// DEPRECATED.
|
// DEPRECATED.
|
||||||
////////////////////////////////
|
////////////////////////////////
|
||||||
|
|||||||
@@ -58,6 +58,11 @@ export async function onFirstRunMaybe() {
|
|||||||
hasRunBefore: true,
|
hasRunBefore: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (process.env.E2E_TEST_BUILD) {
|
||||||
|
writeSettings({
|
||||||
|
isTestMode: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -161,7 +161,10 @@ export default function AppDetailsPage() {
|
|||||||
const fullAppPath = appBasePath.replace("$APP_BASE_PATH", selectedApp.path);
|
const fullAppPath = appBasePath.replace("$APP_BASE_PATH", selectedApp.path);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative min-h-screen p-4 w-full">
|
<div
|
||||||
|
className="relative min-h-screen p-4 w-full"
|
||||||
|
data-testid="app-details-page"
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => router.history.back()}
|
onClick={() => router.history.back()}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
@@ -124,7 +124,9 @@ export default function HomePage() {
|
|||||||
chatId: result.chatId,
|
chatId: result.chatId,
|
||||||
attachments,
|
attachments,
|
||||||
});
|
});
|
||||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
await new Promise((resolve) =>
|
||||||
|
setTimeout(resolve, settings?.isTestMode ? 0 : 2000),
|
||||||
|
);
|
||||||
|
|
||||||
setInputValue("");
|
setInputValue("");
|
||||||
setSelectedAppId(result.app.id);
|
setSelectedAppId(result.app.id);
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import path from "node:path";
|
|||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
|
|
||||||
export function getDyadAppPath(appPath: string): string {
|
export function getDyadAppPath(appPath: string): string {
|
||||||
|
if (process.env.E2E_TEST_BUILD) {
|
||||||
|
return path.join("/tmp", "dyad-apps-test", appPath);
|
||||||
|
}
|
||||||
return path.join(os.homedir(), "dyad-apps", appPath);
|
return path.join(os.homedir(), "dyad-apps", appPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import { createServer } from "http";
|
import { createServer } from "http";
|
||||||
import cors from "cors";
|
import cors from "cors";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
// Create Express app
|
// Create Express app
|
||||||
const app = express();
|
const app = express();
|
||||||
@@ -72,6 +74,38 @@ app.post("/v1/chat/completions", (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the last message starts with "tc=" to load test case file
|
||||||
|
let messageContent = CANNED_MESSAGE;
|
||||||
|
if (
|
||||||
|
lastMessage &&
|
||||||
|
lastMessage.content &&
|
||||||
|
lastMessage.content.startsWith("tc=")
|
||||||
|
) {
|
||||||
|
const testCaseName = lastMessage.content.slice(3); // Remove "tc=" prefix
|
||||||
|
const testFilePath = path.join(
|
||||||
|
__dirname,
|
||||||
|
"..",
|
||||||
|
"..",
|
||||||
|
"..",
|
||||||
|
"e2e-tests",
|
||||||
|
"fixtures",
|
||||||
|
`${testCaseName}.md`,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(testFilePath)) {
|
||||||
|
messageContent = fs.readFileSync(testFilePath, "utf-8");
|
||||||
|
console.log(`* Loaded test case: ${testCaseName}`);
|
||||||
|
} else {
|
||||||
|
console.log(`* Test case file not found: ${testFilePath}`);
|
||||||
|
messageContent = `Error: Test case file not found: ${testCaseName}.md`;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`* Error reading test case file: ${error}`);
|
||||||
|
messageContent = `Error: Could not read test case file: ${testCaseName}.md`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Non-streaming response
|
// Non-streaming response
|
||||||
if (!stream) {
|
if (!stream) {
|
||||||
return res.json({
|
return res.json({
|
||||||
@@ -84,7 +118,7 @@ app.post("/v1/chat/completions", (req, res) => {
|
|||||||
index: 0,
|
index: 0,
|
||||||
message: {
|
message: {
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: CANNED_MESSAGE,
|
content: messageContent,
|
||||||
},
|
},
|
||||||
finish_reason: "stop",
|
finish_reason: "stop",
|
||||||
},
|
},
|
||||||
@@ -97,27 +131,30 @@ app.post("/v1/chat/completions", (req, res) => {
|
|||||||
res.setHeader("Cache-Control", "no-cache");
|
res.setHeader("Cache-Control", "no-cache");
|
||||||
res.setHeader("Connection", "keep-alive");
|
res.setHeader("Connection", "keep-alive");
|
||||||
|
|
||||||
// Split the "hello world" message into characters to simulate streaming
|
// Split the message into characters to simulate streaming
|
||||||
const message = CANNED_MESSAGE;
|
const message = messageContent;
|
||||||
const messageChars = message.split("");
|
const messageChars = message.split("");
|
||||||
|
|
||||||
// Stream each character with a delay
|
// Stream each character with a delay
|
||||||
let index = 0;
|
let index = 0;
|
||||||
|
const batchSize = 8;
|
||||||
|
|
||||||
// Send role first
|
// Send role first
|
||||||
res.write(createStreamChunk("", "assistant"));
|
res.write(createStreamChunk("", "assistant"));
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
if (index < messageChars.length) {
|
if (index < messageChars.length) {
|
||||||
res.write(createStreamChunk(messageChars[index]));
|
// Get the next batch of characters (up to batchSize)
|
||||||
index++;
|
const batch = messageChars.slice(index, index + batchSize).join("");
|
||||||
|
res.write(createStreamChunk(batch));
|
||||||
|
index += batchSize;
|
||||||
} else {
|
} else {
|
||||||
// Send the final chunk
|
// Send the final chunk
|
||||||
res.write(createStreamChunk("", "assistant", true));
|
res.write(createStreamChunk("", "assistant", true));
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
res.end();
|
res.end();
|
||||||
}
|
}
|
||||||
}, 10);
|
}, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start the server
|
// Start the server
|
||||||
|
|||||||
Reference in New Issue
Block a user